在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++标准包含许多处理表达式的值类别的规则。有些规则区分左值和右值。例如,当涉及到过载解决方案时。其他规则区分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
}
其他回答
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)}{}如果您想移动。如果我忽略了移动,编译器会警告我吗!
IMHO,关于其含义的最佳解释给了我们Stroustrup+考虑到Dániel Sándor和Mohan的例子:
斯特鲁斯特鲁普:
现在我非常担心。显然,我们正在走向僵局,或者一团糟或两者兼而有之。我用午餐时间做了一个分析,看看(值的)财产是独立的。只有两个独立财产:具有标识–即地址、指针,用户可以确定两个副本是否相同等。可以从中移动–即,我们可以以某种不确定但有效的状态离开到“副本”的源这使我得出结论:值(使用正则表达式符号技巧,使用大写字母表示否定——我很着急):iM:具有标识,无法从中移动im:具有身份并且可以从中移动(例如,将左值转换为右值引用的结果)Im:没有身份,可以被转移。第四种可能性是IM(没有身份,无法移动)在C++(或者,我认为)任何其他语言中都很有用。除了这三种基本的价值分类,我们有两个明显的概括,与这两个相对应独立财产:i: 具有身份m: 可以从移动这让我把这张图放在黑板上:命名我观察到,我们只有有限的命名自由:左边(标记为iM和i)是人们或多或少拥有的形式称为lvalues和右边的两点(标记为m和Im)是多少有些拘谨的人称为右值。这必须反映在我们的命名中。即,W的左“腿”应具有与左值和W的右“腿”应该有和右值相关的名称。我注意到了整个讨论/问题源于右值引用和移动语义。这些概念根本不存在在Strachey的世界中,只有右值和左值。某人观察到每个值都是左值或右值左值不是右值,右值不是左值深深嵌入我们的意识,非常有用的财产,以及在整个标准草案中都可以找到这种二分法的痕迹。我们所有人都同意我们应该保留那些财产(并使其精确)。这进一步限制了我们的命名选择。我观察到标准库措辞使用rvalue表示m(概括),以便保留标准库W的右下点应命名为右值。这导致了对命名的集中讨论。首先,我们需要决定左值。左值是指iM还是广义i?带路道格·格雷戈,我们列出了核心语言措辞中的位置其中lvalue一词被限定为表示一个或另一个。A.在大多数情况下,在最棘手/最脆弱的文本中列出了列表lvalue当前表示iM。这是左值的经典含义因为“在过去的日子里”,什么都没有动过;这是一个新颖的概念在C++0x中。此外,命名左上角的W值给了我们每个值都是左值或右值,但不是两者都是。所以,W的左上角是左值,右下角是为右值。左下角和右上角是什么?左下点是经典左值的推广,允许移动。所以它是一个广义的左值。我们给它取名glvalue值。你可以对缩写词吹毛求疵,但(我认为)不行逻辑。我们假设在认真使用广义左值无论如何都会被缩写,所以我们最好这样做立即(或风险混淆)。W的右上角较小一般比右下角(现在,一如既往,称为右值)。那个点表示可以移动的对象的原始纯概念因为它不能再次引用(析构函数除外)。我喜欢专门的右值这个短语,而不是广义的右值lvalue,但缩写为prvalue的纯rvalue胜出(和可能是正确的)。因此,W的左腿是左值glvalue和右腿是prvalue和rvalue。顺便提一句每个值都是glvalue或prvalue,但不是两者都是。这将留下W:im;也就是说并且可以移动。我们真的没有任何指导我们为那些神秘的野兽取了个好名字。它们对使用(草案)标准文本的人员,但不太可能成为家喻户晓的名字。我们没有在命名来指导我们,所以我们选择了“x”作为中心,未知奇怪,只有专家,甚至是x级。
由于前面的答案详尽地涵盖了价值类别背后的理论,我想补充一点:你可以实际使用并测试它。
对于值类别的一些实际实验,可以使用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));
一旦你确定了主要类别,混合类别就有点无聊了。
有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,不要费心阅读汇编。我添加了很多编译器,只是为了确保它能在所有常见的编译器中运行。
这些新的表达类别是什么?
FCD(n3092)具有出色的描述:
-左值(历史上称为左值,因为左值可能出现在作业的左侧表达式)指定函数或一个物体。[示例:如果E是指针类型的表达式,则*E是一个左值表达式,表示E所指向的对象或函数点。作为另一个例子,结果调用其返回的函数类型是左值引用是左值-结束示例]-x值(An“eXpiring”值)也指对象,通常在其末端附近生命周期(以便其资源例如移动)。xvalue是某些类型表达式的结果涉及右值参考(8.3.2)[示例:调用返回类型为rvalue引用是一个xvalue-终止示例]-glvalue(“广义”左值)是左值或x值。—右值(历史上称为,因为右值可能出现在作业的右侧表达式)是一个xvalue,一个临时对象(12.2)或其子对象,或与对象-prvalue(“纯”rvalue)为不是xvalue的右值。[示例:调用返回类型不是引用是prvalue。a的值文字,如12、7.3e5或true也是prvalue-结束示例]每一个表达式正好属于中的基本分类此分类法:lvalue、xvalue或prvalue。一个表达式称为其值类别[注:讨论第5条中的每个内置运算符指示其值的类别收益率和价值类别它需要的操作数。例如内置赋值运算符左操作数是左值,并且右操作数是prvalue并作为结果产生左值。用户定义的运算符是函数,以及他们的价值类别预期和收益由以下因素决定它们的参数和返回类型-终止笔记
我建议您阅读整个第3.10节L值和右值。
这些新类别与现有的右值和左值类别有何关联?
再一次:
C++0x中的右值和左值类别是否与C++03中的相同?
右值的语义随着移动语义的引入而不断发展。
为什么需要这些新类别?
以便定义和支持移动构造/分配。