在C ++ 03中,表达式是 rvalue 或左值。
在C ++ 11中,表达式可以是:
两个类别分为五类。
答案 0 :(得分:581)
我想这篇文章可能不是那么简短的介绍:n3055
整个大屠杀始于移动语义。一旦我们有可以移动而不是复制的表达式,突然很容易掌握规则,要求区分可以移动的表达式,以及在哪个方向上。
根据我的猜测,基于草案,r / l值的区别保持不变,只有在移动事物变得混乱的情况下。
他们需要吗?如果我们希望放弃新功能,可能不会。但为了实现更好的优化,我们应该接受它们。
引用n3055:
E
是
指针类型的表达式,然后是*E
是一个左值表达式
E
的对象或功能
点。作为另一个例子,
调用一个函数的结果
返回类型是左值引用
左值。] 有问题的文件是这个问题的一个很好的参考,因为它显示了引入新命名法后标准的确切变化。
答案 1 :(得分:325)
这些新的表达类别是什么?
FCD (n3092)有一个很好的描述:
- 一个左值(所谓的历史,因为左值可能出现在左右 作业的左侧 表达式)指定一个函数或 一个东西。 [例子:如果E是 那么指针类型的表达式 * E是一个左值表达式,指的是E的对象或函数 点。作为另一个例子,结果 调用返回的函数 type是左值引用是一个 左值。 - 例子]
- xvalue(an “eXpiring”值)也指一个 对象,通常接近其末尾 一生(所以它的资源可能是 举动,例如)。 xvalue是 某些表达方式的结果 涉及右值参考(8.3.2)。 [ 示例:调用a的结果 返回类型为的函数 右值参考是一个x值。 -结束 例子]
- 一个glvalue(“一般化” 左值(lvalue)是左值或左值。
- 一个rvalue(所谓的,历史上, 因为右值可以出现在 作业的右侧 表达式)是一个xvalue,一个临时的 对象(12.2)或其子对象,或 与a无关的值 对象
- 一个prvalue(“纯”rvalue)是 一个不是xvalue的rvalue。 [ 示例:调用a的结果 返回类型不是a的函数 参考是一个prvalue。 a的价值 文字如12,7.3e5或true是 也是一个prvalue。 - 例子]
每 表达式恰好属于其中之一 中国的基本分类 这个分类:lvalue,xvalue或 prvalue。这个属性 表达式称为其值 类别。 [注:讨论 第5章中的每个内置运算符 表示它的值的类别 收益率和价值类别 它期望的操作数。例如, 内置赋值运算符期望 左操作数是左值和 右操作数是prvalue 并产生一个左值作为结果。 用户定义的运算符是函数, 以及它们的价值类别 期望和收益率由 他们的参数和返回类型。 -结束 注
我建议您阅读整个 3.10 Lvalues and rvalues 部分。
这些新类别如何与现有的左值和左值类别相关联?
再次:
C ++ 0x中的rvalue和左值类别是否与C ++ 03中的相同?
rvalues的语义特别随着移动语义的引入而发展。
为什么需要这些新类别?
这样就可以定义和支持移动构造/分配。
答案 2 :(得分:172)
我将从你的上一个问题开始:
为什么需要这些新类别?
C ++标准包含许多处理表达式值类别的规则。一些规则区分左值和右值。例如,当涉及到重载决策时。其他规则区分glvalue和prvalue。例如,您可以使用不完整或抽象类型的glvalue,但没有不完整或抽象类型的prvalue。在我们使用这个术语之前,实际需要区分glvalue / prvalue的规则是指lvalue / rvalue和它们无意中是错误的还是包含了很多解释和例外的规则......“除非rvalue是由于未命名右值参考......“。因此,将glvalues和prvalues的概念作为自己的名称似乎是一个好主意。
这些新的表达类别是什么? 这些新类别如何与现有的左值和左值类别相关?
我们仍然有与C ++ 98兼容的术语lvalue和rvalue。我们只是将rvalues分成两个子组,xvalues和prvalues,我们将lvalues和xvalues称为glvalues。 Xvalues是未命名的右值引用的一种新值类别。每个表达式都是以下三个中的一个:左值,右值,右值。维恩图看起来像这样:
______ ______
/ X \
/ / \ \
| l | x | pr |
\ \ / /
\______X______/
gl r
功能示例:
int prvalue();
int& lvalue();
int&& xvalue();
但也不要忘记命名的右值引用是左值:
void foo(int&& t) {
// t is initialized with an rvalue expression
// but is actually an lvalue expression itself
}
答案 3 :(得分:150)
为什么需要这些新类别? WG21众神只是试图迷惑我们凡人吗?
我觉得其他答案(尽管其中很多都是好的)确实能够抓住这个特定问题的答案。是的,这些类别等存在允许移动语义,但复杂性存在的原因之一。这是在C ++ 11中移动东西的一个不可侵犯的规则:
只有在毫无疑问安全的情况下才会移动。
这就是为什么存在这些类别的原因:能够谈论可以安全地离开它们的价值观,并谈论不存在的价值观。
在最早版本的r值参考中,运动很容易发生。 太容易了。当用户并不真正想要的时候,很容易隐藏地移动东西。
以下是可以安全移动的情况:
如果你这样做:
SomeType &&Func() { ... }
SomeType &&val = Func();
SomeType otherVal{val};
这是做什么的?在规范的旧版本中,在5个值进入之前,这将引发一个移动。当然可以。您将rvalue引用传递给构造函数,因此它绑定到采用右值引用的构造函数。这很明显。
这只有一个问题;你没有要求移动它。哦,你可能会说&&
应该是一个线索,但这并没有改变它违反规则的事实。 val
不是临时的,因为临时工没有名字。您可能已延长临时的生命周期,但这意味着它不是临时;它就像任何其他堆栈变量一样。
如果它不是暂时的,并且您没有要求移动它,那么移动错误。
显而易见的解决方案是使val
成为左值。这意味着你无法从中移动。好的;它被命名,所以它是一个左值。
一旦你这样做,你就不能再说SomeType&&
意味着同样的事情。您现在已经区分了命名的右值引用和未命名的右值引用。那么,命名的右值引用是左值;这是我们上面的解决方案。那么我们称之为未命名的右值引用(上面Func
的返回值)?
这不是左值,因为你无法从左值移动。我们需要能够通过返回&&
来移动;你怎么能明确地说要移动一些东西?毕竟,这就是std::move
返回的内容。它不是rvalue(旧式),因为它可以位于等式的左侧(事情实际上有点复杂,请参阅this question和下面的注释)。它既不是左值也不是左值;这是一种新事物。
我们拥有的是一个可以视为左值的值,除了它可以隐式移动。我们称之为xvalue。
请注意,xvalues使我们获得另外两类值:
prvalue实际上只是前一种rvalue的新名称,即它们是不是 xvalues的rvalues。
Glvalues是一个组中xvalues和lvalues的并集,因为它们共享许多属性。
所以真的,这一切都归结为xvalues以及将运动限制在精确且仅限于某些地方的需要。这些地方由右值类别定义; prvalues是隐式移动,xvalues是显式移动(std::move
返回xvalue)。
答案 4 :(得分:117)
恕我直言,关于其含义的最佳解释给了我们Stroustrup +考虑了Dániel Sándor和Mohan的例子:
<强> Stroustrup的:强>
现在我非常担心。很明显,我们正陷入僵局或 一团糟或两者皆有。我花了午餐时间做分析看看哪个 属性(值)是独立的。只有两个 独立财产:
has identity
- 即地址,指针,用户可以确定两个副本是否相同等。can be moved from
- 即我们被允许留下来复制&#34;在一些不确定但有效的状态这使我得出结论,正好有三种 值(使用正则表达式使用大写字母的技巧) 表示否定 - 我匆忙):
iM
:具有身份,无法移动im
:具有身份并且可以移动(例如,将左值转换为右值参考的结果)
Im
:没有身份,可以从中移除。第四种可能性
IM
,(没有身份,不能移动)不是 在C++
(或者,我认为)中用于任何其他语言都很有用。除了这三个基本的价值分类,我们 有两个明显的概括,对应于这两个 独立财产:
i
:有身份- 移出
m
:可以从命名
我发现我们只有有限的自由命名:两点指向 左边(标有
观察到的想法iM
和i
)是人们或多或少的人 形式已经调用了lvalues
和右边的两个点 (标有m
和Im
)是具有或多或少形式的人 已拨打rvalues
。这必须反映在我们的命名中。那是, 左边&#34;腿&#34;W
的{{1}}应该包含与lvalue
相关的名称 对#34;腿&#34;W
应该包含与rvalue.
相关的名称 整个讨论/问题来自于引入 rvalue引用和移动语义。这些概念根本不存在 在Strachey的世界中,只有rvalues
和lvalues
。有人
- 每个
value
都是lvalue
或rvalue
lvalue
不是rvalue
而rvalue
不是lvalue
深深嵌入我们的意识,非常有用的属性,和 在标准草案中可以找到这种二分法的痕迹。我们 所有人都同意我们应该保留这些属性(并制作它们 精确)。这进一步限制了我们的命名选择。我观察到了 标准库措辞使用
rvalue
表示m
( 概括),以便保持期望和文本 标准库应该命名W
的右下角rvalue.
这引发了对命名的集中讨论。首先,我们需要决定 在
lvalue.
lvalue
是iM
还是概括i
? LED 通过Doug Gregor,我们列出了核心语言措辞中的位置 单词lvalue
有资格表示其中一个或另一个。一个 列表是在大多数情况下以及最棘手/最脆弱的文本中制作的lvalue
目前的意思是iM
。这是左值的经典含义 因为&#34;在过去#34;什么都没动;move
是一个新颖的概念 在C++0x
。另外,命名W
lvalue
的topleft点给了我们 每个值都是lvalue
或rvalue
的属性,但不是两者。因此,
W
的左上角是lvalue
,右下角是 是rvalue.
左下角和右上角是什么? 左下角是经典左值的推广, 允许移动。所以它是generalized lvalue.
我们命名它glvalue.
你可以对缩写进行狡辩,但(我认为)不是 与逻辑。我们假设认真使用generalized lvalue
无论如何都会以某种方式缩写,所以我们最好这样做 立即(或冒险混淆)。 W的右上角较少 一般而不是右下角(现在,一如既往地称为rvalue
)。那 point代表您可以移动的对象的原始纯概念 因为它不能再次引用(除了析构函数)。 与specialized rvalue
相比,我喜欢短语generalized lvalue
,但缩写为pure rvalue
的{{1}}赢了(和 可能是正确的)。所以,W的左腿是prvalue
和lvalue
而右腿是glvalue
和prvalue
顺便说一句, 每个值都是glvalue或prvalue,但不是两者。这会留下
rvalue.
:W
的顶部中间位置;也就是说,有的价值观 身份和可以移动。我们真的没有任何指导 我们为那些神秘的野兽命名。它们很重要 使用(草案)标准文本的人,但不太可能 成为家喻户晓的名字。我们没有发现任何真正的限制 命名指导我们,所以我们选择'x'作为中心,未知, 很奇怪,只有xpert,甚至是x-rated。
答案 5 :(得分:36)
ISOC ++ 11(官方ISO / IEC 14882:2011)是C ++编程语言标准的最新版本。它包含一些新功能和概念,例如:
如果我们想要了解新表达式值类别的概念,我们必须知道有rvalue和左值引用。 最好知道rvalues可以传递给非const rvalue引用。
int& r_i=7; // compile error
int&& rr_i=7; // OK
如果我们引用N3337工作草案(最相似的草案到已公布的ISOC ++ 11标准)中标题为Lvalues和rvalues的小节,我们可以对价值类别的概念有一些直觉。
3.10 Lvalues和rvalues [basic.lval]
1表达式根据图1中的分类法进行分类。
- 左值(所谓的,历史上,因为左值可能出现在赋值表达式的左侧)指定一个函数 或一个物体。 [例子:如果E是指针类型的表达式,那么 * E是一个左值表达式,指的是E指向的对象或函数。作为另一个例子,调用函数的结果 其返回类型是左值引用是左值。 - 示例]
- xvalue(“eXpiring”值)也指对象,通常接近其生命周期的末尾(以便可以移动其资源, 例)。 xvalue是某些表达式的结果 涉及右值参考(8.3.2)。 [例子:调用的结果 返回类型为右值引用的函数是xvalue。 -结束 例子]
- glvalue(“generalized”左值)是左值或x值。
- 一个右值(历史上所谓的,因为右值可能出现在赋值表达式的右侧)是一个x值,一个左右 临时对象(12.2)或其子对象,或不是
的值 与对象相关联。- prvalue(“纯”rvalue)是一个不是xvalue的rvalue。 [示例:调用返回类型不是
的函数的结果 参考是一个prvalue。文字的值,例如12,7.3e5或
true也是一个prvalue。 - 示例]每个表达式都属于基本的一个 此分类中的分类:lvalue,xvalue或prvalue。这个 表达式的属性称为其值类别。
但我不太确定这个小节是否足以清楚地理解这些概念,因为&#34;通常&#34;并不是一般的,&#34;接近其生命周期的结束&#34;实际上并不具体,&#34;涉及右值参考&#34;并不是很清楚,&#34;示例:调用返回类型为右值引用的函数的结果是xvalue。&#34;听起来像一条蛇咬着它的尾巴。
每个表达式都属于一个主要值类别。这些值类别是左值,右值和右值类别。
当且仅当E指的是ALREADY具有使其可在E之外访问的身份(地址,名称或别名)的实体时,表达式E属于左值类别。
#include <iostream>
int i=7;
const int& f(){
return i;
}
int main()
{
std::cout<<&"www"<<std::endl; // The expression "www" in this row is an lvalue expression, because string literals are arrays and every array has an address.
i; // The expression i in this row is an lvalue expression, because it refers to the same entity ...
i; // ... as the entity the expression i in this row refers to.
int* p_i=new int(7);
*p_i; // The expression *p_i in this row is an lvalue expression, because it refers to the same entity ...
*p_i; // ... as the entity the expression *p_i in this row refers to.
const int& r_I=7;
r_I; // The expression r_I in this row is an lvalue expression, because it refers to the same entity ...
r_I; // ... as the entity the expression r_I in this row refers to.
f(); // The expression f() in this row is an lvalue expression, because it refers to the same entity ...
i; // ... as the entity the expression f() in this row refers to.
return 0;
}
当且仅当它是
时,表达式E属于xvalue类别- 调用函数的结果,无论是隐式还是显式,其返回类型是对返回的对象类型的右值引用,或
int&& f(){
return 3;
}
int main()
{
f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.
return 0;
}
- 对对象类型的右值引用的强制转换,或
int main()
{
static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).
return 0;
}
- 一个类成员访问表达式,指定非引用类型的非静态数据成员,其中对象表达式是xvalue,或
struct As
{
int i;
};
As&& f(){
return As();
}
int main()
{
f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.
return 0;
}
- 指向成员的表达式,其中第一个操作数是xvalue,第二个操作数是指向数据成员的指针。
请注意,上述规则的效果是对对象的命名rvalue引用被视为lvalues,对对象的未命名rvalue引用被视为xvalues;对函数的右值引用被视为左值,无论是否命名。
#include <functional>
struct As
{
int i;
};
As&& f(){
return As();
}
int main()
{
f(); // The expression f() belongs to the xvalue category, because it refers to an unnamed rvalue reference to object.
As&& rr_a=As();
rr_a; // The expression rr_a belongs to the lvalue category, because it refers to a named rvalue reference to object.
std::ref(f); // The expression std::ref(f) belongs to the lvalue category, because it refers to an rvalue reference to function.
return 0;
}
当且仅当E既不属于左值也不属于xvalue类别时,表达式E属于prvalue类别。
struct As
{
void f(){
this; // The expression this is a prvalue expression. Note, that the expression this is not a variable.
}
};
As f(){
return As();
}
int main()
{
f(); // The expression f() belongs to the prvalue category, because it belongs neither to the lvalue nor to the xvalue category.
return 0;
}
还有两个重要的混合价值类别。这些值类别是rvalue和glvalue类别。
当且仅当E属于xvalue类别或属于prvalue类别时,表达式E属于rvalue类别。
请注意,此定义表示当且仅当E指的是没有任何标识使其可在E YET之外访问的实体时,表达式E属于右值类别。
当且仅当E属于左值类别或xvalue类别时,表达式E属于glvalue类别。
Scott Meyer published有一个非常有用的经验法则来区分rvalues和左值。
- 如果您可以获取表达式的地址,则表达式为左值。
- 如果表达式的类型是左值引用(例如,T&amp;或const T&amp;等),则该表达式是左值。
- 否则,表达式是右值。从概念上(通常也实际上),rvalues对应于临时对象,例如 作为从函数返回或通过隐式类型创建的 转换。大多数文字值(例如10和5.3)也是右值。
答案 6 :(得分:34)
C ++ 03的类别太受限制,无法正确地将rvalue引用引入表达式属性。
随着它们的引入,据说未命名的右值引用求值为rvalue,这样重载解析更喜欢rvalue引用绑定,这将使它选择移动构造函数而不是复制构造函数。但是发现这会导致问题,例如Dynamic Types和资格问题。
要显示此信息,请考虑
int const&& f();
int main() {
int &&i = f(); // disgusting!
}
在pre-xvalue草案中,这是允许的,因为在C ++ 03中,非类类型的rvalues永远不会被cv限定。但是const
意图适用于rvalue-reference情况,因为这里我们做引用对象(= memory!),并且从非类rvalues中删除const主要是为了因为周围没有物体。
动态类型的问题具有类似的性质。在C ++ 03中,类类型的rvalues具有已知的动态类型 - 它是该表达式的静态类型。因为要以另一种方式,你需要引用或解引用,它们评估为左值。对于未命名的右值引用,情况并非如此,但它们可以显示多态行为。所以要解决它,
未命名的右值引用变为 xvalues 。它们可以是合格的,并且可能具有不同的动态类型。它们像预期的那样在重载期间更喜欢rvalue引用,并且不会绑定到非const左值引用。
之前的rvalue(文字,由非强制转换为非引用类型创建的对象)现在变为 prvalue 。它们在重载期间具有与xvalues相同的优先级。
之前的左值是一个左值。
完成两个分组以捕获那些可以合格且可以具有不同动态类型( glvalues )的分组以及那些重载优先于右值引用绑定( rvalues )的分组。
答案 7 :(得分:24)
我一直在努力解决这个问题,直到我遇到cppreference.com对value categories的解释。
它实际上相当简单,但我发现它通常以难以记忆的方式解释。这里非常示意性地解释。我将引用页面的某些部分:
主要类别
主要值类别对应于表达式的两个属性:
具有标识:可以确定表达式是否与另一个表达式引用相同的实体,例如通过比较对象的地址或它们识别的函数(直接或间接获得) );
可以从移动:移动构造函数,移动赋值运算符或其他实现移动语义的函数重载可以绑定到表达式。
表达:
- 具有身份并且无法移动被称为左值表达式;
- 具有身份并且可以被移动称为 xvalue表达式;
- 没有身份且可以移动被称为 prvalue表达式;
- 没有身份,不能被移动不被使用。
左值
左值(“左值”)表达式是具有标识并且无法从移动的表达式。
rvalue(直到C ++ 11),prvalue(从C ++ 11开始)
prvalue(“纯rvalue”)表达式是没有身份的表达式,可以从移动。
x值
xvalue(“expiring value”)表达式是具有标识并且可以从移动的表达式。
glvalue
glvalue(“generalized lvalue”)表达式是一个左值或x值的表达式。它有身份。它可能会也可能不会被移走。
rvalue(自C ++ 11起)
rvalue(“right value”)表达式是prvalue或xvalue的表达式。它可以从移动。它可能有也可能没有身份。
答案 8 :(得分:16)
这些新类别如何与现有的左值和左值类别相关?
C ++ 03左值仍然是C ++ 11左值,而C ++ 03右值在C ++ 11中称为prvalue。
答案 9 :(得分:14)
上面的优秀答案的一个附录,即使在我阅读了Stroustrup并且认为我理解rvalue / lvalue区别之后,这一点让我很困惑。当你看到
int&& a = 3
,
将int&&
作为一种类型进行阅读非常诱人,并得出a
是左值的结论。它不是:
int&& a = 3;
int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
int& b = a; //compiles
a
有一个名称,并且事实上是一个左值。不要将&&
视为a
类型的一部分;它只是告诉你允许a
绑定到什么的东西。
这对构造函数中的T&&
类型参数尤其重要。如果你写
Foo::Foo(T&& _t) : t{_t} {}
您将_t
复制到t
。你需要
Foo::Foo(T&& _t) : t{std::move(_t)} {}
如果你想搬家。当我遗漏move
时,我的编译器会警告我吗?
答案 10 :(得分:9)
这些是 C++ 委员会用来定义 C++11 中移动语义的术语。 Here's the story。
鉴于这些术语的精确定义、long lists of rules 或这张流行的图表,我发现它们很难理解:
在带有典型示例的维恩图上更容易:
基本上:
现在,好问题是,如果我们有两个正交属性(“具有身份”和“可以移动”),那么完成左值、x值和右值的第四个类别是什么? 那将是一个没有身份(因此以后无法访问)且无法移动(需要复制其值)的表达式。这根本没有用,所以没有命名。
答案 11 :(得分:2)
由于前面的答案详尽地涵盖了价值类别背后的理论,所以我想补充一件事:您可以实际使用它并进行测试。
要对值类别进行一些动手实验,可以使用decltype specifier。它的行为明确区分了三个主要值类别(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 on value category中的所有示例。
以下是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(trunk). Clang 7.0.0 and msvc 19.16 are doing fine.
static_assert(IS_XVALUE(S().x));
弄清楚主要类别后,混合类别很无聊。
有关更多示例(和实验),请查看以下link on compiler explorer。不过,不要打扰阅读程序集。我添加了很多编译器,只是为了确保它可以在所有常见的编译器中正常工作。