在C ++ 03中,表达式是rvalue或lvalue。
在C ++ 11中,表达式可以是:
- 右值
- 左值
- 值
- 总值
- 前值
两个类别已变成五个类别。
- 这些新的表达类别是什么?
- 这些新类别与现有的右值和左值类别有何关系?
- C ++ 0x中的rvalue和lvalue类别与C ++ 03中的类别相同吗?
- 为什么需要这些新类别?是WG21神只是想迷惑我们凡人?
string("hello") = string("world")
。
在C ++ 03中,表达式是rvalue或lvalue。
在C ++ 11中,表达式可以是:
两个类别已变成五个类别。
string("hello") = string("world")
。
Answers:
我猜这个文档可能只是一个简短的介绍:n3055
整个屠杀始于移动语义。一旦我们有了可以移动且不能复制的表达式,突然容易掌握的规则就要求在可以移动的表达式之间以及在哪个方向上进行区分。
根据我对草案的猜测,r / l值的区别保持不变,只是在移动事物变得混乱的情况下。
他们需要吗?如果我们希望放弃这些新功能,可能不会。但是为了更好的优化,我们可能应该接受它们。
引用n3055:
E
是指针类型的表达式,则*E
是指向所指向的对象或函数的左值表达式E
。又例如,调用返回类型为左值引用的函数的结果为左值。] 所讨论的文档对于该问题是一个很好的参考,因为它显示了由于引入了新术语所导致的标准的确切变化。
这些新的表达类别是什么?
的FCD(n3092)具有优异的描述:
—一个左值(历史上称为,因为左值可能出现在赋值表达式的左侧)指定一个函数或一个对象。[示例:如果E是指针类型的表达式,那么* E是一个左值表达式,表示E指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果为左值。—结束示例]
— xvalue(“ eXpiring”值)也指对象,通常接近其生命周期的结尾(例如,其资源可以移动)。xvalue是某些包含rvalue引用(8.3.2)的表达式的结果。[示例:调用返回类型为右值引用的函数的结果为xvalue。—结束示例]
— glvalue(“通用”左值)是左值或x值。
—一个右值(之所以称为历史值,是因为右值可能出现在赋值表达式的右侧)是一个x值,一个临时对象(12.2)或其子对象,或者是一个与对象无关的值。
— prvalue(“纯” rvalue)是不是xvalue的rvalue。[示例:调用返回类型不是引用的函数的结果是prvalue。文字的值(例如12、7.3e5或true)也是prvalue。—结束示例]
每个表达式恰好属于该分类法的基本分类之一:lvalue,xvalue或prvalue。表达式的此属性称为其值类别。[注意:第5章对每个内置运算符的讨论都指出了它产生的值的类别以及期望的操作数的值类别。例如,内置赋值运算符期望左操作数是左值,右操作数是pr值并产生左值作为结果。用户定义的运算符是函数,它们期望和产生的值的类别由其参数和返回类型确定。—尾注
我建议您阅读整个章节3.10 Lvalues和rvalues。
这些新类别与现有的右值和左值类别有何关系?
再次:
C ++ 0x中的rvalue和lvalue类别与C ++ 03中的类别相同吗?
rvalues的语义已经特别随着移动语义的引入而发展。
为什么需要这些新类别?
这样就可以定义和支持移动构造/分配。
glvalue
为lvalue
和lvalue
作为plvalue
,是一致的?
我将从您的最后一个问题开始:
为什么需要这些新类别?
C ++标准包含许多处理表达式的值类别的规则。一些规则区分左值和右值。例如,涉及过载解析。其他规则区分glvalue和prvalue。例如,您可以有一个具有不完整或抽象类型的glvalue,但没有具有不完整或抽象类型的prvalue。在使用此术语之前,实际上需要区分称为lvalue / rvalue的glvalue / prvalue的规则,它们要么是无意的错误,要么包含很多解释和例外,例如“ ......除非rvalue是由于未命名引起的”右值参考...”。因此,仅给glvalues和prvalues概念命名是一个好主意。
这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关系?
我们仍然有与C ++ 98兼容的术语左值和右值。我们只是将右值分为两个子组,即x值和pr值,并将左值和x值称为glvalue。Xvalues是一种用于未命名的rvalue引用的新型值类别。每个表达式都是以下三个之一:lvalue,xvalue,prvalue。维恩图如下所示:
______ ______
/ 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
}
为什么需要这些新类别?WG21众神只是在试图使我们仅仅是凡人吗?
我不认为其他答案(虽然很多)很好地抓住了这个特定问题的答案。是的,存在这些类别等以允许移动语义,但是存在复杂性是出于一个原因。这是在C ++ 11中移动内容的一条严格规则:
只有在绝对安全的情况下,才可以移动。
这就是为什么存在这些类别的原因:能够在安全地脱离价值的地方谈论价值,而在不安全的地方谈论价值。
在最早的r值引用版本中,移动很容易发生。太容易了 足够容易的是,当用户没有真正意图时,隐式移动物体的可能性很大。
在某些情况下可以安全移动某些东西:
如果您这样做:
SomeType &&Func() { ... }
SomeType &&val = Func();
SomeType otherVal{val};
这是做什么的?在较早版本的规范中,在引入5个值之前,这会引起很大的变化。当然可以。您将右值引用传递给构造函数,因此它绑定到采用右值引用的构造函数。很明显
这只是一个问题。您没有要求移动它。哦,您可能会说&&
应该是一个线索,但这并不能改变它违反规则的事实。val
不是临时的,因为临时人员没有名字。您可能延长了临时文件的寿命,但这意味着它不是临时文件。就像其他任何堆栈变量一样。
如果它不是临时的,并且您没有要求移动它,那么移动是错误的。
显而易见的解决方案是创建val
一个左值。这意味着您不能离开它。好的; 它被命名,所以它是一个左值。
一旦这样做,您将再也无法说这SomeType&&
意味着无处不在。现在,您已经在命名右值引用和未命名右值引用之间进行了区分。好吧,命名的右值引用就是左值;那就是我们上面的解决方案。那么我们怎么称呼未命名的右值引用(Func
上面的返回值)?
它不是左值,因为您不能从左值移动。并且我们需要能够通过返回a来移动&&
; 您还能如何明确地说出要移动的东西?std::move
毕竟,那才是回报。它不是右值(旧式),因为它可以位于等式的左侧(实际上实际上更复杂一些,请参见下面的问题和注释)。它既不是左值也不是右值;这是一种新事物。
我们拥有的是一个值,您可以将其视为左值,但它可以隐式地从中移出。我们称它为xvalue。
请注意,x值使我们获得了另外两类值:
prvalue实际上只是先前类型的rvalue的新名称,即它们是不是 xvalues的rvalue。
Glvalues是一组xvalues和lvalues的并集,因为它们确实共享许多共同的属性。
所以说真的,这全都归结为xvalues以及将移动限制在准确且仅某些位置的需求。这些位置由右值类别定义;prvalues是隐式移动,xvalues是显式移动(std::move
返回xvalue)。
&&
。
X foo(); foo() = X;
例如- …由于这个基本原因,我不能完全遵循上面的出色答案,因为您实际上只能区分基于新xvalue和旧式prvalue,因为它可以位于lhs上。
X
上课 X foo();
是一个函数声明,并且foo() = X();
是一行代码。(我离开的第二组括号中foo() = X();
在我上面的评论。)因为我只是贴有这种用法强调一个问题,看到stackoverflow.com/questions/15482508/...
恕我直言,关于其含义的最好解释是给我们Stroustrup +考虑了DánielSándor和Mohan的例子:
Stroustrup:
现在我很担心。显然,我们将陷入僵局,一团糟或两者兼而有之。我花了午饭时间进行分析,以查看(值)哪些属性是独立的。只有两个独立的属性:
has identity
–即和地址,指针,用户可以确定两个副本是否相同,等等。can be moved from
–即允许我们以某种不确定但有效的状态离开“副本”的来源这导致我得出结论,确切地说,存在三种值(使用正则表达式用大写字母表示否定的技巧,我很着急):
iM
:具有身份,不能从im
:具有标识并且可以从中移出(例如,将左值强制转换为右值引用的结果)
Im
:没有身份,可以移动。第四种可能性,
IM
(没有身份且无法移动C++
)在任何其他语言中(或我认为)都没有用。除了这三个基本的值分类,我们还有两个明显的概括,它们对应于两个独立的属性:
i
:具有身份m
:可以从命名
我观察到,我们只有有限的命名自由:左边的两个点(标记为
iM
和i
)是正规化人员所呼唤的东西lvalues
,右边的两个点(标记为m
和Im
)是正规化程度高的人称呼的 东西。打电话了rvalues
。这必须在我们的命名中得到体现。也就是说,的左“腿”W
应具有与之相关的名称,lvalue
而右的“腿”W
应具有与之相关的名称。rvalue.
我注意到,整个讨论/问题都源于右值引用和移动语义的引入。这些概念在Strachey的由justrvalues
和组成的世界中根本不存在lvalues
。有人观察到
- 每个
value
都是一个lvalue
或一个rvalue
- An
lvalue
不是anrvalue
,anrvalue
不是Anlvalue
在我们的意识中深深地扎根,非常有用的特性,并且在整个标准草案中都可以找到这种二分法的痕迹。我们都同意,我们应该保留这些属性(并使其精确)。这进一步限制了我们的命名选择。我观察到标准库的措辞
rvalue
用来表示m
(概括),以便保留标准库的期望和文本,W
应将的右下角命名为rvalue.
这引起了对命名的集中讨论。首先,我们需要再决定
lvalue.
是否应lvalue
均值iM
或泛化i
?在道格·格里戈尔(Doug Gregor)的带领下,我们在核心语言措辞中列出了该词lvalue
有资格代表一个或另一个的位置。列出了清单,并且在大多数情况下,当前最棘手/最脆弱的文字lvalue
表示iM
。这是左值的经典含义,因为“过去”什么都没有移动。move
是的一个新概念C++0x
。此外,命名的左上角点W
lvalue
为我们提供了以下属性:每个值都是anlvalue
或anrvalue
,但不能同时是两者。因此,
W
is的左上角点和islvalue
的右下角点是rvalue.
什么使左下角点和右上角点呢?左下角是经典左值的概括,允许移动。因此,它是一个“generalized lvalue.
我们将其命名为”。glvalue.
您可以质疑缩写,但是(我认为)不符合逻辑。我们假设严重使用generalized lvalue
会以某种方式被缩写,因此我们最好立即进行使用(否则会造成混淆)。W的右上角点不如右下角(现在称为rvalue
)普遍。该点代表可以移动的对象的原始纯概念,因为无法再次引用它(析构函数除外)。我喜欢这个短语specialized rvalue
,generalized lvalue
但是pure rvalue
缩写为prvalue
胜出(可能是正确的)。因此,W的左腿lvalue
和glvalue
和右腿prvalue
和rvalue.
顺便说一句,每个值可以是一个glvalue或prvalue,但不能同时使用。这叶子的顶部中间
W
:im
; 也就是说,具有标识并且可以移动的值。我们真的没有任何东西可以引导我们为那些神秘的野兽取一个好名字。对于使用(草稿)标准文本的人来说,它们很重要,但不太可能成为家喻户晓的名字。我们在命名上没有发现任何实际限制来指导我们,因此我们选择了“ x”作为中心,未知,奇怪,仅xpert甚至是x级。
lvalue
s,所有其他文字是prvalue
s。严格来说,您可以说非字符串文字不可移动,但这并不是标准的编写方式。
ISOC ++ 11(正式为ISO / IEC 14882:2011)是C ++编程语言标准的最新版本。它包含一些新功能和概念,例如:
如果我们想了解新表达式值类别的概念,我们必须知道有右值和左值引用。最好知道右值可以传递给非常量右值引用。
int& r_i=7; // compile error
int&& rr_i=7; // OK
如果引用工作草案N3337(与已发布的ISOC ++ 11标准最相似的草案)中名为Lvalues和rvalues的小节,则可以对值类别的概念有所了解。
3.10左值和右值[basic.lval]
1根据图1中的分类法对表达式进行分类。
- 左值(之所以称为历史值,是因为左值可能出现在赋值表达式的左侧)指定函数或对象。[示例:如果E是指针类型的表达式,那么* E是一个左值表达式,表示E指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果为左值。—结束示例]
- xvalue(“ eXpiring”值)也指对象,通常在其生命周期即将结束时(例如,可以移动其资源)。xvalue是某些包含rvalue引用(8.3.2)的表达式的结果。[示例:调用返回类型为右值引用的函数的结果为xvalue。—结束示例]
- glvalue(“广义”左值)是左值或x值。
- 一个右值(在过去,由于右值可能出现在赋值表达式的右侧,因此被称为)是一个x值,一个
临时对象(12.2)或其子对象,或者是一个
与对象无关的值。- prvalue(“纯” rvalue)是不是xvalue的rvalue。[示例:调用返回类型不是
引用的函数的结果是prvalue。文字的值(例如12、7.3e5或
true)也是prvalue。—结束示例]每个表达式恰好属于该分类法的基本分类之一:lvalue,xvalue或prvalue。表达式的此属性称为其值类别。
但是我不太确定这个小节是否足以清楚地理解这些概念,因为“通常”不是很笼统,“寿命即将结束”不是很具体,“涉及右值引用”也不是很清楚,和“示例:调用返回类型为rvalue引用的函数的结果为xvalue。” 听起来像蛇在咬它的尾巴。
每个表达式恰好属于一个主值类别。这些值类别是lvalue,xvalue和prvalue类别。
当且仅当E引用的实体已经具有使其可以在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;
}
—一个类成员访问表达式,它指定一个非引用类型的非静态数据成员,其中对象表达式是一个x值,或者
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,第二个操作数是指向数据成员的指针。
请注意,上述规则的作用是将对对象的命名右值引用视为左值,而对对象的未命名右值引用视为xvalue。不论是否命名,对函数的右值引用均视为左值。
#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才属于右值类别。
请注意,此定义意味着,当且仅当E引用的实体没有任何使其在E YET之外可访问的标识时,该表达E才属于右值类别。
当且仅当E属于左值类别或xvalue类别时,表达式E才属于glvalue类别。
斯科特·迈耶(Scott Meyer)发布了一个非常有用的经验法则,以区分右值与左值。
- 如果可以接受表达式的地址,则该表达式为左值。
- 如果表达式的类型是左值引用(例如T&或const T&等),则该表达式是左值。
- 否则,表达式为右值。从概念上(通常也是实际上),右值对应于临时对象,例如从函数返回的对象或通过隐式类型转换创建的对象。大多数文字值(例如10和5.3)也是右值。
struct As{void f(){this;}}
的this
变量是prvalue。我认为this
应该是左值。直到标准9.3.2声明:在非静态(9.3)成员函数的主体中,关键字this是prvalue表达式。
this
是prvalue但是*this
lvalue
C ++ 03的类别太受限制,无法正确地将右值引用引入表达式属性中。
引入它们后,据说未命名的右值引用的计算结果为右值,因此重载解析将首选右值引用绑定,这将使其选择移动构造器而不是复制构造器。但是我们发现,这会在所有方面引起问题,例如动态类型和限定条件。
为了说明这一点,请考虑
int const&& f();
int main() {
int &&i = f(); // disgusting!
}
在xvalue之前的草稿中,这是允许的,因为在C ++ 03中,非类类型的rvalue永远都不是cv限定的。但它打算const
适用于右值引用的情况,因为在这里我们做引用了对象(=内存!),而从非类右值中删除const的主要原因是周围没有对象。
动态类型的问题具有相似的性质。在C ++ 03中,类类型的右值具有已知的动态类型-它是该表达式的静态类型。因为要用另一种方法,所以需要引用或取消引用,它们的值均为左值。未命名的右值引用并非如此,但是它们可以显示多态行为。所以要解决它,
未命名的右值引用变为xvalues。它们可以是合格的,并且动态类型可能不同。它们确实像预期的那样在重载期间更喜欢右值引用,并且不会绑定到非常量左值引用。
以前是右值(文字,通过强制转换为非引用类型创建的对象)现在变为prvalue。在重载期间,它们与xvalues具有相同的首选项。
以前是左值的保持左值。
然后进行了两个分组,以捕获那些可以限定,可以具有不同动态类型(glvalues)的分组以及那些重载首选rvalue引用绑定(rvalues)的分组。
我为此苦苦挣扎了很长时间,直到遇到cppreference.com对值类别的解释。
它实际上相当简单,但是我发现它经常以难以记住的方式进行解释。在此非常示意性地解释。我将引用页面的某些部分:
主要类别
主值类别对应于表达式的两个属性:
具有身份:可以通过比较对象的地址或它们标识的功能(直接或间接获得)来确定该表达式是否与另一个表达式引用相同的实体;
可以从以下位置移动:移动构造函数,移动赋值运算符,或实现移动语义的另一个函数重载可以绑定到表达式。
表示为:
- 具有身份且无法从中移出称为左值表达式 ;
- 具有身份并可以从中移出称为xvalue表达式 ;
- 没有身份并且可以从中移出称为prvalue表达式 ;
- 没有身份并且无法从中移走的人不会被使用。
左值
左值(“左值”)表达式是具有标识并且不能从中移动的表达式。
rvalue(直到C ++ 11),prvalue(从C ++ 11开始)
prvalue(“ pure rvalue”)表达式是不具有标识并且可以从移出的表达式。
值
xvalue(“到期值”)表达式是具有标识并且可以从移出的表达式。
总值
glvalue(“广义左值”)表达式是左值或x值的表达式。它具有身份。它可能会被移走,也可能不会被移走。
右值(自C ++ 11起)
rvalue(“正确的值”)表达式是prvalue或xvalue的表达式。它可以被移动。它可能具有或不具有身份。
这些新类别与现有的右值和左值类别有何关系?
C ++ 03左值仍然是C ++ 11左值,而C ++ 03左值在C ++ 11中称为prvalue。
上面的出色答案的一个附录,甚至在我读完Stroustrup并认为我理解右值/左值之间的区别后,仍然使我感到困惑。当你看到
int&& a = 3
,
将int&&
a作为类型读取并得出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
!时,编译器会警告我吗!
a
允许绑定到什么”:当然可以,但是在第2行和第3行中,您的变量是c&b,并且不是绑定到a的变量,a
这里的类型无关紧要,不是吗?如果a
声明,则行将相同int a
。这里实际的主要区别是,在1号线一不必须const
绑定到3
由于前面的答案详尽地涵盖了价值类别背后的理论,因此我想补充一件事:您可以实际使用它并进行测试。
对于一些使用值类别的动手实验,您可以使用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的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 9.1. Clang 7.0.0 and msvc 19.16 are doing fine.
static_assert(IS_XVALUE(S().x));
一旦弄清了主要类别,混合类别就很无聊了。
有关更多示例(和实验),请查看以下有关编译器资源管理器的链接。不过,不要打扰阅读程序集。我添加了很多编译器,只是为了确保它可以在所有常见的编译器中正常工作。
#define IS_GLVALUE(X) IS_LVALUE(X) || IS_XVALUE(X)
实际上应该#define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X))
看看如果你们&&
两个会发生什么IS_GLVALUE
。