如何识别Rvalue或Lvalue引用以及if-it-has-a-name规则

18

我正在阅读Thomas Becker关于右值引用和其使用的文章。在其中,他定义了所谓的如果它有一个名字(if-it-has-a-name)规则:

声明为右值引用的东西可以是左值或右值。区分的标准是:如果它有一个名字,那么它就是一个左值。否则,它就是一个右值。

对我来说,这听起来非常合理。它也清楚地识别了右值引用的右值特性。

我的问题是:

  1. 您是否同意这个规则?如果不同意,您能举出违反此规则的例子吗?
  2. 如果没有违规情况,我们能否使用此规则来定义表达式的右值/左值特性?

2
这也等同于“如果你可以获取一个表达式的地址,那么它就是一个左值”。但这并不正确。然而,否定形式是正确的:“如果你无法获取一个表达式的地址,那么它就是一个右值”。 - Nawaz
2
@Nawaz 不完全正确... 你不能取一个位域的地址,但它可能是一个左值。C++极其复杂的本质再次显现 :-) - Brian Bi
1
@mustafa:这是一个例子:http://coliru.stacked-crooked.com/a/599453432bbbc7b4 - Nawaz
1
@Nawaz,感谢您提供(病态的)示例。 - Mustafa
2
@MattMcNabb 在C++中,你可以取register变量的地址。 - T.C.
显示剩余3条评论
3个回答

12

这是用来解释 lvalue 和 rvalue 之间区别的最常见的“经验法则”之一。

在 C++ 中,情况比这复杂得多,因此这只是一个经验法则。我将尝试概括一些概念,并尝试说明为什么在 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中发生了什么变化

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是什么以及什么不是,我们不能避免使用标准中使用的确切术语和规则。这就是它们被编写的原因!


9

虽然这个规则适用于大多数情况,但我不能完全同意它:

对匿名指针的解引用没有名称,但它是一个左值:

foo(*new X);  // Not allowed if foo expects an rvalue reference (example of the article)

基于标准,并考虑到临时对象作为右值的特殊情况,我建议更新规则的第二句话:

"……标准是:如果它指定的是一个不是临时性质的函数或对象,则它是左值。……"


1
你的新规则不起作用。你可以将非临时变量强制转换为右值。 - Edward Strange
1
有没有一种简单且明确的方法来识别具有临时性质的函数/对象? - Mustafa
标准的第12.3节列出了创建临时对象的特定情况。我不确定是否可以用一句话概括,但我会说“临时对象是在表达式中创建的对象,在表达式结束后不再保留”。这包括匿名对象、编译器为表达式评估、转换等目的而创建的对象...你认为呢? - Christophe

6
问题1:该规则严格指的是分类右值引用类型的表达式,而不是一般表达式。在这个上下文中,我几乎同意它(“几乎”是因为还有一些细微差别,请参见下面的引用)。精确措辞在标准中的注释[第5条第7段]中:

总的来说,这个规则的作用是将命名的右值引用视为左值,将未命名的右值引用 对象 视为 xvalue;无论是否命名,右值引用 函数 都被视为左值。

(强调是我自己加的,出于显而易见的原因)


问题2: 从其他答案和评论中可以看出(其中有一些很好的例子),关于表达式值类别的概括性简洁陈述存在问题。以下是我对此的看法。
我们需要从另一方面来看待这个问题:不是试图指定哪些表达式是左值,而是列出哪些是右值;左值就是“其他所有东西”。
首先,为了使事情清晰明了,给出一些定义:
- “对象”指的是数据存储区域,而不是函数或引用(这是标准中的定义)。 - 当我说一个表达式“生成”某个东西时,我的意思是它不仅仅是命名或引用它,而且实际上构造并返回它,作为运算符、函数调用(可能是构造函数调用)或转换(可能是隐式转换)的结果。
现在,基于[3.10](但也涉及标准中的许多其他地方),一个表达式是右值当且仅当它是以下之一:
  1. 一个与对象无关的值(如this或字面量7,不是字符串);
  2. 通过值生成对象的表达式,也称为临时对象
  3. 生成指向对象的rvalue引用的表达式;
  4. 递归地,使用rvalue之一的以下表达式:
    • x.y,其中x是rvalue,y是非静态成员对象
    • x.*y,其中x是rvalue,y是指向成员对象的指针;
    • x[y],其中xy是数组类型的rvalue(使用内置的[]运算符)。

就是这样。

嗯,从技术上讲,以下特殊情况也是rvalue,但我认为它们在实践中并不相关:

  1. 函数调用返回 void,强制类型转换为 void,或者 throw(显然不是左值,我不确定在实践中为什么会对它们的值类别感兴趣);
  2. 其中之一是 obj.mfptr->mfobj.*pmfptr->*pmfmf 是非静态成员函数,pmf 是指向成员函数的指针);这里我们严格讲的是这些形式,而不是可以使用它们构建的函数调用表达式,你真的不能对这些做任何事情,除了进行函数调用,这是完全不同的表达式(需要应用上述规则)。

就是这样了。其他所有都是lvalue。我发现用这种方式推理表达式很容易,因为上面的所有类别都很容易辨认。例如,看一个表达式,排除上述情况,并决定它是一个lvalue是很容易的。即使对于第4类,其描述更长,表达式也很容易识别(我尽力让它成为一行代码,但最终失败了)。

涉及操作符的表达式可以是lvalue或rvalue,具体取决于使用的确切操作符。内置操作符指定每种情况下会发生什么,但用户定义的操作符函数可以改变规则。确定表达式的值类别时,表达式的结构和涉及的类型都很重要。


注释:

  • 关于类别1:
    • 在示例中,this指的是指针值,而不是*this
    • 字符串字面量是lvalue,因为它们是静态存储期的数组,所以它们不适合类别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中是左值,在C++14中是xvalue - issue 1213。)
  • 将rvalue进一步分为xvalue和prvalue对于C++14相对简单:类别1、2、5和6是prvalue,3和4是xvalue。对于C++11,情况略有不同:类别4被分为prvalue、xvalue和lvalue(如上所述,也作为issue 616的解决方案的

    所有引用均指N4140,即发布前的最后一个C++14草案。

    我最初在这里找到了最后两个特殊的右值引用情况here(当然,标准中也有,但更难找)。请注意,该页面上并非所有内容都适用于C++14。它还包含了关于主要值类别背后原理的非常好的摘要(位于顶部)。


你认为枚举常量是值吗? - dyp
“给定声明 int f(int),表达式 f(7) 通过值生成一个对象。” 你确定吗?我认为非类、非数组类型的 prvalue 不是对象。 - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接