在C++03中,表达式要么是右值,要么是左值。
在C++11中,表达式可以是:
右值左值x值glvalue值prvalue值
两个类别变成了五个类别。
这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关联?C++0x中的右值和左值类别是否与C++03中的相同?为什么需要这些新类别?WG21众神只是想迷惑我们这些凡人吗?
在C++03中,表达式要么是右值,要么是左值。
在C++11中,表达式可以是:
右值左值x值glvalue值prvalue值
两个类别变成了五个类别。
这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关联?C++0x中的右值和左值类别是否与C++03中的相同?为什么需要这些新类别?WG21众神只是想迷惑我们这些凡人吗?
当前回答
这是我为我正在写的一本高度可视化的C++书制作的Venn图,我很快将在开发期间在leanpub上发布这本书。
其他答案用文字更详细,并显示类似的图表。但希望这些信息的介绍是相当完整的,并且对参考也很有用。
在这个主题上,我的主要收获是表达式具有这两个财产:身份和可动性。第一个涉及事物存在的“坚固性”。这一点很重要,因为C++抽象机被允许并鼓励通过优化来积极地更改和压缩代码,这意味着没有身份的东西可能只会在编译器或寄存器中存在片刻,然后才会被践踏。但是,如果你回收它的内部,这样的数据也保证不会引起问题,因为没有办法尝试使用它。因此,移动语义被发明出来,允许我们捕获对临时变量的引用,将其升级为lvalues并延长其寿命。
移动语义最初不仅仅是为了浪费时间,而是为了让它们可以被其他人使用。
当你把你的玉米面包送人时,你送人的人现在拥有它。他们会吃掉它。一旦你送人,你不应该试图吃或消化这些玉米面包。也许玉米面包本来是往垃圾堆里去的,但现在是往他们的肚子里去了。它不再是你的了。
在C++领域,“消耗”资源的想法意味着资源现在归我们所有,因此我们应该进行任何必要的清理,并确保对象不会在其他地方访问。通常情况下,这意味着借用勇气来创建新对象。我称之为“捐献器官”。通常,我们讨论的是对象中包含的指针或引用,或者类似的东西,我们希望保留这些指针或引用的位置,因为它们引用的是程序中其他未消亡的数据。
因此,您可以编写一个接受rvalue引用的函数重载,如果传入了临时(prvalue),则将调用该重载。一个新的左值将在绑定到函数所取的右值引用时创建,从而延长临时值的寿命,以便您可以在函数中使用它。
在某一时刻,我们意识到,我们经常会在一个范围内处理完左值非临时数据,但却希望在另一个范围中进行分解。但它们不是右值,因此不会绑定到右值引用。所以我们做了std::move,这只是一个从左值到右值引用的花式转换。这样的数据是一个xvalue:一个以前的lvalue,现在就像它是一个临时的,所以它也可以从中移动。
其他回答
为什么需要这些新类别?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)。
由于前面的答案详尽地涵盖了价值类别背后的理论,我想补充一点:你可以实际使用并测试它。
对于值类别的一些实际实验,可以使用decltype说明符。它的行为明确区分了三个主要值类别(xvalue、lvalue和prvalue)。
使用预处理器可以节省我们一些键入。。。
主要类别:
#define IS_XVALUE(X) std::is_rvalue_reference<decltype((X))>::value
#define IS_LVALUE(X) std::is_lvalue_reference<decltype((X))>::value
#define IS_PRVALUE(X) !std::is_reference<decltype((X))>::value
混合类别:
#define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X))
#define IS_RVALUE(X) (IS_PRVALUE(X) || IS_XVALUE(X))
现在,我们可以(几乎)复制值类别上cppreference中的所有示例。
以下是C++17的一些示例(用于简洁的static_assert):
void doesNothing(){}
struct S
{
int x{0};
};
int x = 1;
int y = 2;
S s;
static_assert(IS_LVALUE(x));
static_assert(IS_LVALUE(x+=y));
static_assert(IS_LVALUE("Hello world!"));
static_assert(IS_LVALUE(++x));
static_assert(IS_PRVALUE(1));
static_assert(IS_PRVALUE(x++));
static_assert(IS_PRVALUE(static_cast<double>(x)));
static_assert(IS_PRVALUE(std::string{}));
static_assert(IS_PRVALUE(throw std::exception()));
static_assert(IS_PRVALUE(doesNothing()));
static_assert(IS_XVALUE(std::move(s)));
// The next one doesn't work in gcc 8.2 but in gcc 9.1. Clang 7.0.0 and msvc 19.16 are doing fine.
static_assert(IS_XVALUE(S().x));
一旦你确定了主要类别,混合类别就有点无聊了。
有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,不要费心阅读汇编。我添加了很多编译器,只是为了确保它能在所有常见的编译器中运行。
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)的对象。
这些新类别与现有的右值和左值类别有何关联?
C++03左值仍然是C++11左值,而C++03右值在C++11中称为prvalue。
上面优秀答案的一个补充,即使在我读过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)}{}如果您想移动。如果我忽略了移动,编译器会警告我吗!