在C++03中,表达式要么是右值,要么是左值。

在C++11中,表达式可以是:

右值左值x值glvalue值prvalue值

两个类别变成了五个类别。

这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关联?C++0x中的右值和左值类别是否与C++03中的相同?为什么需要这些新类别?WG21众神只是想迷惑我们这些凡人吗?


当前回答

C++03的类别太有限,无法将右值引用正确引入表达式属性。

随着它们的引入,据说未命名的右值引用求值为右值,因此重载解析将倾向于右值引用绑定,这将使其选择移动构造函数而不是复制构造函数。但研究发现,这会导致各种问题,例如动态类型和限定。

要显示这一点,请考虑

int const&& f();

int main() {
  int &&i = f(); // disgusting!
}

在xvalue之前的草稿中,这是允许的,因为在C++03中,非类类型的rvalue永远不会被cv限定。但const适用于右值引用情况,因为这里我们确实引用了对象(=内存!),从非类右值中删除const主要是因为没有对象。

动态类型的问题具有相似的性质。在C++03中,类类型的右值有一个已知的动态类型——它是该表达式的静态类型。因为要以另一种方式实现,您需要引用或解引用,它们的计算结果为左值。对于未命名的右值引用,情况并非如此,但它们可以显示多态行为。为了解决这个问题,

未命名的右值引用变为xvalue。它们可以是限定的,并且可能具有不同的动态类型。它们确实像预期的那样,在重载期间更喜欢右值引用,并且不会绑定到非常值左值引用。以前是一个rvalue(文本,通过对非引用类型的强制转换创建的对象)现在变成了一个prvalue。在重载期间,它们与xvalue具有相同的偏好。以前的左值仍然是左值。

我们进行了两个分组,以捕获那些可以被限定并且可以具有不同动态类型(glvalue)的对象,以及那些重载更喜欢右值引用绑定(rvalues)的对象。

其他回答

上面优秀答案的一个补充,即使在我读过Stroustrup并认为我理解了右值/左值的区别之后,这一点也让我感到困惑。当你看到

int&&a=3,

将int&&作为一种类型阅读,并得出a是一个右值的结论是非常诱人的。它不是:

int&& a = 3;
int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
int& b = a; //compiles

a有一个名字,实际上是一个左值。不要将&&视为a;类型的一部分;它只是告诉你什么是允许绑定的。

这对构造函数中的T&&类型参数尤为重要。如果你写

Foo::Foo(T&&_T):T{_T}{}

你将把t复制到t中。你需要

Foo::Foo(T&&_T):T{std::move(_T)}{}如果您想移动。如果我忽略了移动,编译器会警告我吗!

这些新类别与现有的右值和左值类别有何关联?

C++03左值仍然是C++11左值,而C++03右值在C++11中称为prvalue。

C++03的类别太有限,无法将右值引用正确引入表达式属性。

随着它们的引入,据说未命名的右值引用求值为右值,因此重载解析将倾向于右值引用绑定,这将使其选择移动构造函数而不是复制构造函数。但研究发现,这会导致各种问题,例如动态类型和限定。

要显示这一点,请考虑

int const&& f();

int main() {
  int &&i = f(); // disgusting!
}

在xvalue之前的草稿中,这是允许的,因为在C++03中,非类类型的rvalue永远不会被cv限定。但const适用于右值引用情况,因为这里我们确实引用了对象(=内存!),从非类右值中删除const主要是因为没有对象。

动态类型的问题具有相似的性质。在C++03中,类类型的右值有一个已知的动态类型——它是该表达式的静态类型。因为要以另一种方式实现,您需要引用或解引用,它们的计算结果为左值。对于未命名的右值引用,情况并非如此,但它们可以显示多态行为。为了解决这个问题,

未命名的右值引用变为xvalue。它们可以是限定的,并且可能具有不同的动态类型。它们确实像预期的那样,在重载期间更喜欢右值引用,并且不会绑定到非常值左值引用。以前是一个rvalue(文本,通过对非引用类型的强制转换创建的对象)现在变成了一个prvalue。在重载期间,它们与xvalue具有相同的偏好。以前的左值仍然是左值。

我们进行了两个分组,以捕获那些可以被限定并且可以具有不同动态类型(glvalue)的对象,以及那些重载更喜欢右值引用绑定(rvalues)的对象。

为什么需要这些新类别?WG21众神只是想迷惑我们这些凡人吗?

我觉得其他的答案(虽然很多都很好)并没有真正抓住这个问题的答案。是的,这些类别的存在是为了允许移动语义,但复杂性的存在有一个原因。这是C++11中移动内容的一条不可侵犯的规则:

只有在毫无疑问安全的情况下,你才能移动。

这就是为什么存在这些类别:能够在安全的地方谈论价值观,而在不安全的地方讨论价值观。

在最早版本的r值引用中,移动很容易发生。太容易了。很容易,当用户不是真的想这样做的时候,就有很大的潜力来隐式移动东西。

以下是移动物品的安全情况:

当它是临时对象或其子对象时。(prvalue)当用户明确表示要移动它时。

如果您这样做:

SomeType &&Func() { ... }

SomeType &&val = Func();
SomeType otherVal{val};

这有什么作用?在较旧版本的规范中,在5个值出现之前,这将引发一个变化。当然了。您向构造函数传递了一个右值引用,因此它绑定到接受右值引用的构造函数。这很明显。

这只是一个问题;你没有要求移动它。哦,你可能会说&&应该是一个线索,但这并不能改变它违反规则的事实。val不是临时的,因为临时人员没有名字。你可能延长了暂时的生命周期,但这意味着它不是暂时的;它就像任何其他堆栈变量一样。

如果这不是暂时的,并且你没有要求移动它,那么移动是错误的。

显而易见的解决方案是使val成为左值。这意味着你不能离开它。好的,好的;它被命名,所以它是一个左值。

一旦你做到了这一点,你就不能再说SomeType&&在任何地方都意味着相同的东西。现在,您已经区分了命名的右值引用和未命名的右值引用。嗯,命名的右值引用是左值;这就是我们上面的解决方案。那么我们怎么称呼未命名的右值引用(上面Func的返回值)呢?

它不是左值,因为你不能从左值移动。我们需要能够通过返回&&;你怎么能明确地说要搬东西?毕竟,这就是std::move返回的结果。这不是一个右值(老式),因为它可以位于方程的左侧(事实上情况有点复杂,请参阅下面的问题和注释)。它既不是左值,也不是右值;这是一种新事物。

我们所拥有的是一个可以作为左值处理的值,除了它可以从中隐式移动。我们称之为xvalue。

注意,xvalues是我们获得其他两类值的原因:

prvalue实际上只是先前类型的rvalue的新名称,即它们是不是xvalue的rvalues。Glvalues是xvalues和lvalue在一个组中的联合,因为它们确实共享许多共同的财产。

所以,实际上,这一切都归结为xvalue和将移动限制在特定位置的需要。这些地方由右值类别定义;prvalues是隐式移动,xvalues是显式移动(std::move返回一个xvalue)。

我将从你的最后一个问题开始:

为什么需要这些新类别?

C++标准包含许多处理表达式的值类别的规则。有些规则区分左值和右值。例如,当涉及到过载解决方案时。其他规则区分glvalue和prvalue。例如,可以有一个glvalue具有不完整或抽象类型,但没有prvalue具有不完全或抽象类型。在我们使用这个术语之前,实际上需要区分glvalue/prvalue的规则称为lvalue/rvalue,它们要么是无意中错误的,要么包含大量对规则的解释和例外“……除非rvalue是由于未命名的rvalue引用……”。因此,只给glvalues和prvalues的概念起自己的名字似乎是个好主意。

这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关联?

我们仍然有与C++98兼容的术语lvalue和rvalue。我们将右值分为两个子组,xvalue和prvalue,我们将左值和xvalue称为glvalue。Xvalues是未命名右值引用的一种新的值类别。每个表达式都是这三个值之一:lvalue、xvalue、prvalue。Venn图如下所示:

    ______ ______
   /      X      \
  /      / \      \
 |   l  | x |  pr  |
  \      \ /      /
   \______X______/
       gl    r

函数示例:

int   prvalue();
int&  lvalue();
int&& xvalue();

但也不要忘记,命名的rvalue引用是lvalues:

void foo(int&& t) {
  // t is initialized with an rvalue expression
  // but is actually an lvalue expression itself
}