我正在阅读Thomas Becker关于右值引用和其使用的文章。在其中,他定义了所谓的如果它有一个名字(if-it-has-a-name)规则:
声明为右值引用的东西可以是左值或右值。区分的标准是:如果它有一个名字,那么它就是一个左值。否则,它就是一个右值。
对我来说,这听起来非常合理。它也清楚地识别了右值引用的右值特性。
我的问题是:
- 您是否同意这个规则?如果不同意,您能举出违反此规则的例子吗?
- 如果没有违规情况,我们能否使用此规则来定义表达式的右值/左值特性?
我正在阅读Thomas Becker关于右值引用和其使用的文章。在其中,他定义了所谓的如果它有一个名字(if-it-has-a-name)规则:
声明为右值引用的东西可以是左值或右值。区分的标准是:如果它有一个名字,那么它就是一个左值。否则,它就是一个右值。
对我来说,这听起来非常合理。它也清楚地识别了右值引用的右值特性。
我的问题是:
这是用来解释 lvalue 和 rvalue 之间区别的最常见的“经验法则”之一。
在 C++ 中,情况比这复杂得多,因此这只是一个经验法则。我将尝试概括一些概念,并尝试说明为什么在 C++ 世界中这个问题如此复杂。首先,让我们回顾一下曾经发生过什么。
首先,"lvalue" 和 "rvalue" 最初在编程语言领域中是什么意思?
在像 C 或 Pascal 这样的简单语言中,这些术语用于指代可以被放置在赋值运算符的左边或右边的东西。
在像 Pascal 这样的语言中,赋值不是表达式,而仅是语句,因此差异非常明显,并且以语法术语定义。lvalue 是变量的名称,或数组下标。
这是因为只有这两个东西能够出现在赋值运算符的左边:
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++中,你不能仅仅通过观察语法结构来确定表达式是否会产生lvalue。例如:
f() = g();
如果例如f()
返回一个引用,在C语言中可能没有意义,但在C++中完全合法,这就是像v[i] = j
这样的表达式适用于std::vector
的原因: operator []
返回对元素的引用,因此可以将其赋值。
那么现在还有什么区别lvalue和rvalue的意义吗? 当然,对于基本类型仍然有区别,但也可以决定什么可以绑定到非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中已经存在的内容。规则已经比“如果有名称,则为lvalue”更复杂,因为您必须考虑引用。因此,返回非const引用的表达式仍被认为是lvalue。
另外,这里提到的其他经验法则并不适用于所有情况。例如,“如果可以取其地址,则为lvalue”。如果您所说的“取地址”是指“应用operator&”,那么它可能有效,但不要欺骗自己认为您永远不会拥有临时变量的地址:例如,临时成员函数中的“this”指针将指向它。
C++11通过添加“rvalue引用”的概念将更多的复杂性放入了bin中,即引用可以绑定到rvalue上,即使非const也是如此。它只能应用于rvalue的事实使其既安全又有用。我认为没有必要解释为什么需要rvalue引用,所以继续。
问题在于现在我们有更多的情况要考虑。那么现在什么是rvalue呢?标准实际上区分了不同类型的rvalue,以便能够正确地说明rvalue引用的行为以及在存在rvalue引用时的重载解析和模板参数推导。因此,我们有像xvalue,prvalue之类的术语,这使得事情更加复杂。
因此,“所有具有名称的内容都是lvalue”仍然可能是正确的,但肯定不是每个lvalue都具有名称。返回非const lvalue引用的函数是一个lvalue。返回值创建临时对象并且是rvalue的函数,所以返回rvalue引用的函数也是rvalue。
“临时变量是rvalue”怎么样?这是正确的,但通过简单地强制转换类型(正如std :: move所做的那样),也可以将非临时变量变为rvalue。
因此,我认为如果我们记住它们是经验法则,所有这些规则都是有用的。它们总会有一些边角案例不适用,因为要准确地指定rvalue是什么以及什么不是,我们不能避免使用标准中使用的确切术语和规则。这就是它们被编写的原因!
虽然这个规则适用于大多数情况,但我不能完全同意它:
对匿名指针的解引用没有名称,但它是一个左值:
foo(*new X); // Not allowed if foo expects an rvalue reference (example of the article)
基于标准,并考虑到临时对象作为右值的特殊情况,我建议更新规则的第二句话:
"……标准是:如果它指定的是一个不是临时性质的函数或对象,则它是左值。……"
总的来说,这个规则的作用是将命名的右值引用视为左值,将未命名的右值引用 对象 视为 xvalue;无论是否命名,右值引用 函数 都被视为左值。
(强调是我自己加的,出于显而易见的原因)
this
或字面量7
,不是字符串);x.y
,其中x
是rvalue,y
是非静态成员对象;x.*y
,其中x
是rvalue,y
是指向成员对象的指针;x[y]
,其中x
或y
是数组类型的rvalue(使用内置的[]
运算符)。就是这样。
嗯,从技术上讲,以下特殊情况也是rvalue,但我认为它们在实践中并不相关:
void
,强制类型转换为 void
,或者 throw
(显然不是左值,我不确定在实践中为什么会对它们的值类别感兴趣);obj.mf
、ptr->mf
、obj.*pmf
或 ptr->*pmf
(mf
是非静态成员函数,pmf
是指向成员函数的指针);这里我们严格讲的是这些形式,而不是可以使用它们构建的函数调用表达式,你真的不能对这些做任何事情,除了进行函数调用,这是完全不同的表达式(需要应用上述规则)。就是这样了。其他所有都是lvalue。我发现用这种方式推理表达式很容易,因为上面的所有类别都很容易辨认。例如,看一个表达式,排除上述情况,并决定它是一个lvalue是很容易的。即使对于第4类,其描述更长,表达式也很容易识别(我尽力让它成为一行代码,但最终失败了)。
涉及操作符的表达式可以是lvalue或rvalue,具体取决于使用的确切操作符。内置操作符指定每种情况下会发生什么,但用户定义的操作符函数可以改变规则。确定表达式的值类别时,表达式的结构和涉及的类型都很重要。
注释:
this
指的是指针值,而不是*this
。int& f(int)
,表达式f(7)
不通过值生成对象,因此不适合类别2;它确实生成了一个引用,但它不是右值引用,因此也不适用于类别3;该表达式是左值。int&& f(int)
,表达式f(7)
生成了一个右值引用;类别3适用于这里,因此该表达式是右值。int f(int)
,表达式f(7)
通过值生成了一个对象;类别2适用于这里,该表达式是右值。int&& a
,使用表达式a
不会生成一个右值引用;它只是使用了引用类型的标识符。类别3不适用,该表达式是左值。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
是右值引用。所有引用均指N4140,即发布前的最后一个C++14草案。
我最初在这里找到了最后两个特殊的右值引用情况here(当然,标准中也有,但更难找)。请注意,该页面上并非所有内容都适用于C++14。它还包含了关于主要值类别背后原理的非常好的摘要(位于顶部)。
int f(int)
,表达式 f(7)
通过值生成一个对象。” 你确定吗?我认为非类、非数组类型的 prvalue 不是对象。 - dyp
register
变量的地址。 - T.C.