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

时间:2014-12-18 19:33:39

标签: c++ c++11 rvalue-reference lvalue rvalue

我正在阅读Thomas Becker关于右值参考及其使用的article。在那里,他定义了他所谓的 if-it-has-a-name 规则:

  

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

这对我来说听起来很合理。它还清楚地标识了右值参考的右值。

我的问题是:

  1. 你同意这个规则吗?如果没有,您能举例说明可以违反此规则吗?
  2. 如果没有违反此规则的行为。我们可以使用此规则来定义表达式的rvalueness / lvaluness吗?

3 个答案:

答案 0 :(得分:9)

虽然规则涵盖了大多数情况,但我不能同意它:

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

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

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

  

" ......标准是:如果它指定一个函数或一个对象   这不是暂时性的,那么它就是一个左值。 ......"。

答案 1 :(得分:9)

这是最常见的经验法则之一"用于解释左值和右值之间的区别。

C ++中的情况比这复杂得多,所以这不过是经验法则。我将尝试恢复几个概念,并尝试明确为什么这个问题在C ++世界中如此复杂。首先让我们回顾一下曾经发生的事情

开头有C

首先,"左值"和" rvalue"一般来说,在编程语言的世界中,这意味着什么?

使用更简单的语言(如C或Pascal),这些术语用于表示可以放置在赋值运算符的 L eft或 R 处的内容。

在像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语言中没有意义但在C ++中完全合法的语句,例如,f()返回引用。这就像v[i] = j这样的表达式如何适用于std::vectoroperator[]返回对该元素的引用,以便您可以为其分配。

那么重要的是区分左值和右值是什么意思?区别仍然与基本类型的课程相关,但也决定了可以绑定到非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中曾经发生过的事情。这些规则已经比#34更复杂了;如果它有一个名字,它就是一个左值",因为你必须考虑引用。因此,返回非const引用的表达式仍被视为左值。

此外,这里提到的其他经验法则在所有情况下都不起作用。例如"如果你可以拿它的地址,它就是一个左值"。如果通过"取地址"你的意思是"应用operator&"然后它可能有用,但不要欺骗自己以为你无法获得临时的地址:例如,临时的成员函数中的this指针将指向它。

C ++ 11中的变化

C ++ 11通过添加右值引用的概念将更多的复杂性放入bin中,即,即使非const也可以绑定到rvalue的引用。它只能 应用于右值这一事实使它既安全又有用。我不认为它需要解释为什么右值参考是有用的,所以继续。

现在的重点是,现在我们需要考虑更多的案例。那么现在rvalue是什么?标准实际上区分了不同类型的rvalues,以便能够在rvalue引用的情况下正确地说明rvalue引用的行为和重载决策以及模板参数推导。所以我们有像xvalueprvalue这样的术语以及类似的东西,这会使事情变得更复杂。

我们的经验法则是什么?

所以"所有有名字的都是左值"仍然可以是真的,但是肯定每个左值都有一个名字是不对的。返回非常量左值引用的函数是左值。按值返回某个函数的函数会创建一个临时函数,它是一个rvalue,因此返回一个rvalue引用的函数也是如此。

" temporaries是rvalues"。这是真的,但也可以通过简单地转换类型将非临时值变成右值(如std::move所示)。

所以我认为如果我们记住它们是什么,所有这些规则都是有用的:经验法则。 他们总是会有一些他们不适用的角落案例,因为要准确指定一个r值是什么,不是什么,我们不能避免使用中使用的确切条款和规则。标准。这就是他们写作的原因!

答案 2 :(得分:4)

问题1:该规则严格意义上是对右值引用类型的表达式进行分类,而不是通常的表达式。我几乎同意在这种情况下('几乎'因为还有更多内容,请参阅下面的引文)。准确的措辞载于标准[第5条第7款]的注释中:

  

通常,此规则的作用是命名的右值引用   被视为左值和未命名的右值引用到对象   被视为xvalues;右值引用到函数被视为   是否命名左值。

(强调我的,显而易见的原因)


问题2:从其他答案和评论(其中有一些很好的例子)可以看出,关于表达式的值类别的一般,简洁陈述存在问题。这是我思考它的方式。

我们需要从另一方面看问题:不是试图指定哪些表达式是左值,列出了rvalues的种类;左值是其他一切

首先,有几个定义可以保持清晰:

  • 对象表示数据的存储区域,而非函数而非参考(它是标准中的定义)。
  • 当我说一个表达式生成的东西时,我的意思是它不仅仅是命名它或引用它,而是实际构造并返回它作为运算符,函数组合的结果调用(可能是构造函数调用)或强制转换(可能是隐式强制转换)。

现在,主要基于[3.10](但也是标准中的其他几个地方),表达式是右值当且仅当它是以下之一:

  1. 与对象无关的(如this或文字,如7,而不是字符串);
  2. 按值生成对象的表达式,a.k.a。临时对象;
  3. 生成对象的右值引用的表达式;
  4. 递归地,使用rvalue的以下表达式之一:
    • x.y,其中x是右值,y是非静态成员对象;
    • x.*y,其中x是右值,y是指向成员对象的指针;
    • x[y],其中xy是数组类型的右值(使用内置的[]运算符)。
  5. 那就是它。

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

    1. 函数调用返回void,强制转换为voidthrow(显然不是左值,我不确定为什么我会感兴趣在实践中的价值类别);
    2. obj.mf之一,ptr->mfobj.*pmfptr->*pmfmf是非静态成员函数,pmf是指向成员函数的指针);在这里我们严格谈论这些形式,可以用它们构建的函数调用表达式,你真的不能这些形式的任何东西但是进行一个函数调用,这是一个完全不同的表达式(我们需要应用上面的规则)。
    3. 真的是这样。其他一切都是左值。我发现以这种方式推理表达式很容易,因为上面的所有类别都很容易识别。例如,可以很容易地查看表达式,排除上述情况,并确定它是左值。即使对于具有较长描述的类别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是左值(由于*x,它不适合类别4,它不适合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。)
      • 对于C ++ 14,进一步将rvalues分成xvalues和prvalues是相对简单的:类别1,2,5和6是prvalues,3和4是xvalues。对于C ++ 11,情况略有不同:类别4在prvalues,xvalues和lvalues之间分配(如上所述进行了更改,也是issue 616的解析的一部分)。这可能很重要,因为它可能会影响您从decltype返回的类型。

      所有引用都是针对N4140,即发布前的最后一个C ++ 14草案。

      我首先找到了最后两个特殊的右撇子案例here(当然,所有内容都在标准中,但很难找到)。请注意,并非该页面上的所有内容都适用于C ++ 14。它还包含关于主要价值类别背后的基本原理的非常好的摘要(在顶部)。