我正在阅读托马斯·贝克尔关于右值参考及其使用的文章.在那里,他定义了他称之为if-it-a-name规则:
声明为右值引用的事物可以是左值或右值.区别标准是:如果它有一个名字,那么它就是一个左值.否则,它是一个右值.
这对我来说听起来很合理.它还清楚地标识了右值参考的右值.
我的问题是:
你同意这个规则吗?如果没有,您能举例说明违反此规则吗?
如果没有违反此规则.我们可以使用此规则来定义表达式的rvalueness/lvaluness吗?
Christophe.. 9
虽然规则涵盖了大多数情况,但我不能同意它:
取消引用匿名指针没有名称,但它是左值:
foo(*new X); // Not allowed if foo expects an rvalue reference (example of the article)
基于该标准,并考虑临时对象为rvalues的特殊情况,我建议更新规则的第二句:
"......标准是:如果它指的是一个不属于临时性质的函数或对象,那么它就是一个左值......".
gigabytes.. 9
这是最常用的"经验法则"之一,用于解释左值和右值之间的区别.
C++中的情况比这复杂得多,所以这不过是经验法则.我将尝试恢复几个概念,并试图弄清楚为什么这个问题在C++世界中如此复杂.首先让我们回顾一下曾经发生的事情
首先,在一般的编程语言世界中,"左值"和"右值"原来是什么意思?
在如C或Pascal更简单的语言,所使用的术语来指代什么可以被放置在大号 EFT或在ř赋值运算符的飞行.
在像Pascal这样的语言中,赋值不仅仅是一个表达式而只是一个语句,它的区别很明显,并且用语法术语来定义.左值是变量的名称或数组的下标.
那是因为只有这两件事可以站在任务的左边:
i := 42; (* ok *) a[i] := 42; (* ok *) 42 := 42; (* no sense *)
在C中,同样的差异适用,并且在某种意义上你仍然可以看到一行代码并告诉表达式是否会产生左值或右值.
i = 42; // ok, a variable *p = 42; // ok, a pointer dereference a[i] = 42; // ok, a subscript (which is a pointer dereference anyway) s->var = 42; // ok, a struct member access
那么C++发生了什么变化?
在C++中,事情变得更加复杂,差异不再是语法,而是涉及类型检查过程,原因有两个:
只要它的类型有适当的重载,一切都可以留在赋值的左边 operator=
参考
所以这意味着在C++中你不能仅仅通过查看它的语法结构来表达表达式是否会产生左值.例如:
f() = g();
是一个在C语言中没有意义的语句,但如果例如f()
返回引用,则在C++中完全合法.这就是表达式如何v[i] = j
工作std::vector
:operator[]
返回对元素的引用,以便您可以分配给它.
那么重要的是区分左值和左值有什么意义呢?区别仍然与基本类型相关,但也决定了什么可以绑定到非const引用.
那是因为你不想拥有像这样的法律代码:
int &x = 42; x = 0; // Have we changed the meaning of a natural number??
因此,语言仔细指定什么是左值和什么不是,然后说只有左值可以绑定到非const引用.所以上面的代码是不合法的,因为整数文字不是左值,因此非const引用不能绑定到它.
请注意,const引用是不同的,因为它们可以绑定到文字和临时文件(并且本地引用甚至可以延长那些临时文件的生命周期):
int const&x = 42; // It's ok
到目前为止,我们只触及了C++ 98中已经发生过的事情.规则已经比"如果它有一个名称它是左值"更复杂,因为你必须考虑引用.因此,返回非const引用的表达式仍被视为左值.
此外,这里提到的其他经验法则在所有情况下都不起作用.例如"如果你可以拿它的地址,它就是一个左值".如果通过"取地址"你的意思是"应用operator&
",那么它可能会起作用,但不要欺骗自己以为你不能拥有临时的地址:this
指针在临时的成员函数中,例如,将指向它.
C++ 11通过添加rvalue引用的概念将更多的复杂性放入bin中,即,即使非const也可以绑定到rvalue的引用.它只能应用于右值这一事实使它既安全又有用.我不认为它需要解释为什么右值参考是有用的,所以继续.
这里的要点是,现在我们需要考虑更多的案例.那么现在rvalue是什么?标准实际上区分了不同类型的rvalues,以便能够在rvalue引用的情况下正确地说明rvalue引用的行为和重载决策以及模板参数推导.因此,我们有类似的术语xvalue
,prvalue
事情就是这样的,这使事情变得更加复杂.
所以"所有有名字的东西都是左值"仍然可以是真的,但是肯定不是每个左值都有一个名字.返回非常量左值引用的函数是左值.按值返回某个函数的函数会创建一个临时函数,它是一个rvalue,因此返回一个rvalue引用的函数也是如此.
那么"临时就是右手"呢?这是真的,但也可以通过简单地转换类型(如同std::move
)将非临时数制成rvalues .
因此,如果我们记住它们是什么,我认为所有这些规则都是有用的:经验法则.他们总是会有一些他们不适用的极端情况,因为要准确指定一个右值是什么,什么不是,我们不能避免使用标准中使用的确切术语和规则.这就是为什么他们写的!
问题1:该规则严格是指对右值引用类型的表达式进行分类,而不是一般的表达式。在这种情况下,我几乎同意这一点(“几乎”是因为它还有更多内容,请参见下面的引用)。确切的措词在标准[条款5第7段]的注释中:
通常,此规则的作用是将已命名的右值引用视为左值,将对对象的未命名的右值引用视为左值。不论是否命名,对函数的右值引用均视为左值。
(出于明显的原因,重点是我的)
问题2:从其他答案和注释(那里有一些漂亮的示例)可以看出,关于表达式的值类别的一般,简洁的陈述存在问题。这是我考虑的方式。
我们需要从另一个角度看问题:不是尝试指定哪些表达式是左值,而是列出了右值的种类;左值是其他一切。
首先,有两个定义可以使事情变得清晰:
一个对象装置存储的一个区域用于数据,而不是一个函数,而不是一个参考(它在标准的定义)。
当我说一个表达式生成某些东西时,我的意思是它不仅是命名或引用它,而是实际上是作为运算符,函数调用(可能是构造函数调用)或强制类型转换(可能是隐式强制类型转换)的组合而构造并返回的)。
现在,主要基于[3.10](但在Standard中还有很多其他地方),当且仅当表达式是以下之一时,该表达式才是右值:
与对象无关的值(如this
,或文字如7
,而不是字符串);
通过值生成对象(又称临时对象)的表达式;
生成对一个对象右引用的表达式;
递归地,使用rvalue的以下表达式之一:
x.y
,其中x
rvalue y
是一个非静态成员对象;
x.*y
,其中x
rvalue y
是指向成员对象的指针;
x[y]
,其中任x
或y
是阵列类型的右值(内置的使用[]
操作符)。
而已。
从技术上讲,以下特殊情况也是右值,但我认为它们在实践中不相关:
函数调用returning void
,强制转换为void
或a throw
(显然不是左值,我不确定为什么我实际上对它们的值类别感兴趣);
之一obj.mf
,ptr->mf
,obj.*pmf
,或ptr->*pmf
(mf
是一个非静态成员函数,pmf
是一个指针,指向成员函数); 在这里,我们严格地讲这些形式,而不是可以用它们构建的函数调用表达式,并且您真的不能对它们进行任何操作,而是进行函数调用,这完全是一个不同的表达式(我们需要对它们进行表达)应用上述规则)。
就是这样。其他所有都是左值。我发现以这种方式推理表达式很容易,因为上面的所有类别都易于识别。例如,很容易查看一个表达式,排除上面的情况,然后确定它是一个左值。即使对于类别4(具有较长的描述),这些表达式也很容易识别(我尽力使它成为单一形式,但最终失败了)。
涉及运算符的表达式可以是左值或右值,具体取决于所使用的确切运算符。内置运算符指定在每种情况下会发生什么,但是用户定义的运算符功能可以更改规则。在确定表达式的值类别时,表达式的结构和所涉及的类型都很重要。
笔记:
关于类别1:
this
在示例中,是指this
指针值,而不是*this
。
字符串文字是左值,因为它们是静态存储持续时间的数组,因此它们不属于类别1(它们与对象相关联)。
与类别2和类别3有关的一些示例:
给定声明int& f(int)
,表达式f(7)
不会按值生成对象,因此它不适合类别2;它的确会生成一个引用,但它不是右值引用,因此类别3也不适用;该表达式是一个左值。
给定声明int&& f(int)
,表达式f(7)
生成右值引用;类别3在这里适用,因此表达式是一个右值。
给定声明int f(int)
,表达式f(7)
将按值生成一个对象;类别2在这里适用,该表达式是一个右值。
对于演员表,我们可以应用与上述三个项目符号相同的推理。
给定声明int&& a
,使用表达式a
不会生成右值引用;它仅使用引用类型的标识符。类别3不适用,该表达式为左值。
Lambda表达式按值生成闭包对象-它们在类别2中。
与类别4相关的一些示例:
x->y
被翻译成(*x).y
。*x
是左值(不适用于以上任何类别)。因此,如果y
是非静态成员对象,x->y
则是一个左值(因为它不适合类别4,因此不适合类别*x
6,因为它只讨论成员函数)。
在中x.y
,如果y
是静态成员,则类别4不适用。这样的表达式即使x
是右值,也始终是左值(6也不适用,因为它涉及的是非静态成员函数)。
在中x.y
,如果y
类型为T&
或T&&
,则它不是成员对象(记住,对象,不是引用,不是函数),因此类别4不适用。这样的表达式始终是左值,即使x
是右值,甚至y
是右值引用也是如此。
类别4在C ++ 11中曾经有些不同,但是我相信这种措辞对C ++ 14是正确的。(如果您坚持要知道,下标到rvalue数组中的结果在C ++ 11中曾经是lvalue,而在C ++ 14中是xvalue,发行1213。)
对于C ++ 14,将rvalue进一步分为xvalue和prvalue相对简单:类别1、2、5和6是prvalue,3和4是xvalue。对于C ++ 11,情况略有不同:类别4在prvalues,xvalues和lvalues之间进行了划分(如上所述,并且作为问题616的解决方案的一部分)。这可能很重要,因为它会影响您从中得到的类型decltype
。
所有参考文献均为N4140,这是发布之前的最新C ++ 14草案。
我首先在这里找到了最后两个特殊的右值情况(当然,标准中也包含了所有内容,但更难找到)。请注意,并非该页面上的所有内容都适用于C ++ 14。它还包含有关主要值类别背后原理的非常不错的摘要(顶部)。
虽然规则涵盖了大多数情况,但我不能同意它:
取消引用匿名指针没有名称,但它是左值:
foo(*new X); // Not allowed if foo expects an rvalue reference (example of the article)
基于该标准,并考虑临时对象为rvalues的特殊情况,我建议更新规则的第二句:
"......标准是:如果它指的是一个不属于临时性质的函数或对象,那么它就是一个左值......".
这是最常用的"经验法则"之一,用于解释左值和右值之间的区别.
C++中的情况比这复杂得多,所以这不过是经验法则.我将尝试恢复几个概念,并试图弄清楚为什么这个问题在C++世界中如此复杂.首先让我们回顾一下曾经发生的事情
首先,在一般的编程语言世界中,"左值"和"右值"原来是什么意思?
在如C或Pascal更简单的语言,所使用的术语来指代什么可以被放置在大号 EFT或在ř赋值运算符的飞行.
在像Pascal这样的语言中,赋值不仅仅是一个表达式而只是一个语句,它的区别很明显,并且用语法术语来定义.左值是变量的名称或数组的下标.
那是因为只有这两件事可以站在任务的左边:
i := 42; (* ok *) a[i] := 42; (* ok *) 42 := 42; (* no sense *)
在C中,同样的差异适用,并且在某种意义上你仍然可以看到一行代码并告诉表达式是否会产生左值或右值.
i = 42; // ok, a variable *p = 42; // ok, a pointer dereference a[i] = 42; // ok, a subscript (which is a pointer dereference anyway) s->var = 42; // ok, a struct member access
那么C++发生了什么变化?
在C++中,事情变得更加复杂,差异不再是语法,而是涉及类型检查过程,原因有两个:
只要它的类型有适当的重载,一切都可以留在赋值的左边 operator=
参考
所以这意味着在C++中你不能仅仅通过查看它的语法结构来表达表达式是否会产生左值.例如:
f() = g();
是一个在C语言中没有意义的语句,但如果例如f()
返回引用,则在C++中完全合法.这就是表达式如何v[i] = j
工作std::vector
:operator[]
返回对元素的引用,以便您可以分配给它.
那么重要的是区分左值和左值有什么意义呢?区别仍然与基本类型相关,但也决定了什么可以绑定到非const引用.
那是因为你不想拥有像这样的法律代码:
int &x = 42; x = 0; // Have we changed the meaning of a natural number??
因此,语言仔细指定什么是左值和什么不是,然后说只有左值可以绑定到非const引用.所以上面的代码是不合法的,因为整数文字不是左值,因此非const引用不能绑定到它.
请注意,const引用是不同的,因为它们可以绑定到文字和临时文件(并且本地引用甚至可以延长那些临时文件的生命周期):
int const&x = 42; // It's ok
到目前为止,我们只触及了C++ 98中已经发生过的事情.规则已经比"如果它有一个名称它是左值"更复杂,因为你必须考虑引用.因此,返回非const引用的表达式仍被视为左值.
此外,这里提到的其他经验法则在所有情况下都不起作用.例如"如果你可以拿它的地址,它就是一个左值".如果通过"取地址"你的意思是"应用operator&
",那么它可能会起作用,但不要欺骗自己以为你不能拥有临时的地址:this
指针在临时的成员函数中,例如,将指向它.
C++ 11通过添加rvalue引用的概念将更多的复杂性放入bin中,即,即使非const也可以绑定到rvalue的引用.它只能应用于右值这一事实使它既安全又有用.我不认为它需要解释为什么右值参考是有用的,所以继续.
这里的要点是,现在我们需要考虑更多的案例.那么现在rvalue是什么?标准实际上区分了不同类型的rvalues,以便能够在rvalue引用的情况下正确地说明rvalue引用的行为和重载决策以及模板参数推导.因此,我们有类似的术语xvalue
,prvalue
事情就是这样的,这使事情变得更加复杂.
所以"所有有名字的东西都是左值"仍然可以是真的,但是肯定不是每个左值都有一个名字.返回非常量左值引用的函数是左值.按值返回某个函数的函数会创建一个临时函数,它是一个rvalue,因此返回一个rvalue引用的函数也是如此.
那么"临时就是右手"呢?这是真的,但也可以通过简单地转换类型(如同std::move
)将非临时数制成rvalues .
因此,如果我们记住它们是什么,我认为所有这些规则都是有用的:经验法则.他们总是会有一些他们不适用的极端情况,因为要准确指定一个右值是什么,什么不是,我们不能避免使用标准中使用的确切术语和规则.这就是为什么他们写的!