在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++委员会用来在C++11中定义移动语义的术语。这就是故事。
我发现很难理解这些术语,因为它们有精确的定义、长长的规则列表或这个流行的图表:
在带有典型示例的Venn图上更容易:
基本上:
每个表达式都是左值或右值必须复制左值,因为它具有标识,所以可以稍后使用可以移动rvalue,因为它是临时的(prvalue)或显式移动的(xvalue)
现在,好问题是,如果我们有两个正交的财产(“有恒等式”和“可以移动”),那么完成左值、xvalue和prvalue的第四个类别是什么?这将是一个没有标识的表达式(因此以后无法访问),并且无法移动(需要复制其值)。这根本没用,所以没有命名。
其他回答
这些新类别与现有的右值和左值类别有何关联?
C++03左值仍然是C++11左值,而C++03右值在C++11中称为prvalue。
由于前面的答案详尽地涵盖了价值类别背后的理论,我想补充一点:你可以实际使用并测试它。
对于值类别的一些实际实验,可以使用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));
一旦你确定了主要类别,混合类别就有点无聊了。
有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,不要费心阅读汇编。我添加了很多编译器,只是为了确保它能在所有常见的编译器中运行。
我一直在纠结这个问题,直到我看到cpreference.com对价值类别的解释。
这其实很简单,但我发现它的解释方式很难记住。这里非常示意性地解释了这一点。我将引用页面的某些部分:
主要类别主要值类别对应于表达式的两个财产:具有同一性:可以通过比较对象的地址或它们标识的函数(直接或间接获得)来确定表达式是否与另一个表达式引用相同的实体;可以从以下位置移动:移动构造函数、移动赋值运算符或实现移动语义的另一个函数重载可以绑定到表达式。表达式:具有标识且不能从中移动的称为左值表达式;具有标识并且可以从中移动的称为xvalue表达式;没有标识并且可以从中移动的称为prvalue表达式;没有标识并且无法从中移动。左值左值(“左值”)表达式是一个具有标识且不能从中移动的表达式。rvalue(直到C++11),prvalue(从C++11开始)prvalue(“pure rvalue”)表达式是一个没有标识的表达式,可以从中移动。x值xvalue(“过期值”)表达式是一个具有标识的表达式,可以从中移动。glvalue值glvalue(“广义左值”)表达式是左值或xvalue的表达式。它有身份。它可以移动,也可以不移动。右值(自C++11以来)右值(“右值”)表达式是一个prvalue或xvalue表达式。它可以从移动。它可能有身份,也可能没有身份。
让我们把它放到一张桌子上:
Can be moved from (= rvalue) | Cannot be moved from | |
---|---|---|
Has identity (= glvalue) | xvalue | lvalue |
No identity | prvalue | not used |
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)的对象。
我想这份文件可以作为一个不那么简短的介绍:n3055
整个屠杀从移动语义开始。一旦我们有了可以移动而不可复制的表达式,突然间,容易掌握的规则要求区分可以移动的表达式和方向。
根据我根据草案的猜测,r/l值的区别保持不变,只有在移动的情况下才会变得混乱。
他们需要吗?如果我们想放弃新功能,可能不会。但为了实现更好的优化,我们可能应该接受它们。
引用n3055:
左值(历史上,因为lvalues可能出现在作业的左侧表达式)指定函数或一个物体。[示例:如果E是指针类型的表达式,则为*E是一个左值表达式,引用E所针对的对象或功能点。作为另一示例调用函数的结果返回类型是左值引用左值。]x值(An“eXpiring”值)也指对象,通常在其末端附近生命周期(以便其资源可以例如被移动)。x值为某些类型的结果涉及右值的表达式参考文献。[示例:调用函数的结果返回类型为右值引用为x值。]glvalue(“广义”左值)是左值或x值。右值(所谓,历史上,因为rvalues可以显示在赋值表达式)是xvalue,临时对象或子对象,或其值不与对象关联。A.prvalue(“纯”右值)是右值这不是xvalue。[示例:调用函数的结果返回类型不是引用是prvalue(压力值)]
所讨论的文件是这个问题的一个很好的参考,因为它显示了由于引入新的命名法,标准发生的确切变化。