C++17的左值和右值

文章摘自我的个人博客C++17左值和右值,可以跳转过去阅读,有什么地方需要我改进的欢迎反馈。

首先我承认自己是个小偷,我已经把别人的知识剽窃过来据为己有了。I confess~

被我偷过的无辜“受害者”列表 (在此,我向他们的无私慷慨表示感谢):

如果你想不劳而获窃取我最后的成果,欢迎你开始阅读。这绝不是把别人的文章拿来总结一下那么简单,有很多我自己的东西。


目录

1 背景


1 背景(Background)

对历史背景不感兴趣或者没有耐心的读者,可直接跳转到下一部分: 2 基本概念

1.1 在其它语言中的历史

术语"lvalue"(左值)和“rvalue”(右值),最早是由Christopher Strachey为CPL(B语言的祖先)编程语言引入。在他1967年这篇颇具影响力的讲义: Fundamental Concepts in Programming Languages, 他首次提到了L-valuesR-values(见p15)。

引用他几句经典的论述:

An L-value represents an area of the store of the computer. We call this a location...
Some locations are addressable but some are not.
The two essential features of a location are that it has a content—i.e. an associated R-value

其大意是说,L-value表示一个位置,R-value表示它的内容。用如今的话说,L-value具有程序可访问的(内存)地址,R-value是内容。可以有趣认为,左值就是一个盛水果的容器,而右值就是容器内的水果。他这一最初的概念,一直沿用至今。

然后,C语言之父丹尼斯·里奇(Dennis Ritchie),使用"lvalue"的概念描述C(见K&R,1978, p183, APPENDIX A, Objects and lvalues)。但是他忽略了"rvalue", 因为“lvalue”和非“lvalue”对C语言来说已经足够了。

1.2 在C++中的发展

再到后来的C++,草案也就有了“lvalue”和“ravlue”,起初,他们也在FCD中沿用了过去常规的含义。

C++规范对这两个术语的精确措辞很难,但是有助于解决一些已知的规范问题(与右值引用有关)。Stroustrup原先不打算做出更改,但是,CWG大多数人都不同意并坚持认为,必须有一些更改或使用新术语来解决已知问题和使规范保持一致。

简单地说,就是因为C++11即将引入的右值引用和移动语义的新特性,导致用原有旧术语lvalue和rvalue就不好解释了。所以他们要重新命名和定义新术语。

这份PDF ,Stroustrup记录了他们讨论和思考的过程。他们找到了问题的关键,因为每个表达式的值都具有两个属性。

每个值都有两个独立的属性:

  • 具有身份(has identity) --比如一个指针,一个地址,用户能决定这两份拷贝是具有身份的。
  • 可移动(can safely be moved from)--就是允许移动之后,脱离拷贝的来源,其状态有效。

如果有人觉得不好理解什么是“具有身份”和“可被移动”,那就看我写的两个解释吧:

基于这两个属性,就有了三种不同的组合结果:

  • 具有身份+不可移动
  • 具有身份+可移动
  • 没有身份+可移动

排列组合,按理说是有第四种可能性的。但是对于第四种“没身份且不可移动”,这种值对C++没有用(以及其它任何语言), 所以就舍弃了。

经过这样的思考也就得出了这样的结论:值拥有两个独立属性,被划分成三个基本的分类;我可以简称为“两属性三类别”。基于这样的概念,也就有了相应的命名。

关系

属性“具有身份”,对应了glvalue(广义的左值)
属性“可移动”,对应了ravlue

基于这两个属性的三种结果结果,分别命名:

  • 具有身份+不可移动 --> lvalue
  • 具有身份+可移动 --> xvalue
  • 没有身份+可移动 --> prvalue

好了,C++表达式类型的历史演变也就解释完毕了。上面的结构图,也就是目前C++17使用的标准。


2 基本概念(Basic concepts)

在过去, C++左值和右值的概念相对简单,过去有一句经典的论述:

Every expression is either an lvalue or an rvalue.
每个表达式不是左值,就是右值。

这句话经常在各种C++文章中看到,但是我得提醒你,自从2011年起(ISOC++11 , ISO/IEC 14882),上面的那句经典论述已经不再成立。

发生了什么事情呢? ISO C++11,新的C++标准引入了新的术语,并且定义了表达式的各种类别。新的分类如下:

值类别 定义
lvalue 一个左值(在历史上,因为左值可能出现在赋值表达式的左侧)指定一个函数或者对象。【例子:如果p是一个表达式的指针类型,那么*p就是一个左值表达式,它引用p所指的一个对象或者函数。另一个例子:调用一个函数,它的返回值类型是左值引用,那么函数的结果是左值。】
xvalue xvalue (an “eXpiring”过期的值)。它也引用一个对象,该对象通常接近它的生命周期(以至于它的资源可被移动)。xvalue是某些包含右值引用的表达式【示例:函数返回类型为右值引用的是xvalue。】
glvalue (“generalized” lvalue, 广义的左值),它是一个lvalue或者xvalue
rvalue (在历史上,因为右值可能出现在赋值表达式的右侧)是一个xvalue,一个临时对象或子对象,或没有与任何对象关联的值。
prvalue (“pure” rvalue) 是一个ralue,但不是xvalue. 【比如函数的调用结果,其返回值不是一个引用,那就是prvalue;再比如字面的12,7.e5, 或者ture 都是prvalue】

详细的英文定义在这里: n3055, 第五页。

下面的结构图展示了不同分类之间的关系,请仔细揣摩:

relationships between the categories

换韦恩图(Venn diagram)表示:

exp_categlory

从集合关系:prvalue是rvalue的子集,lvalue是glvalue的子集

前面给出了比较规范的定义,我用更为通俗的话重新定义一次:

值类别 通俗的解释
lvalue 具有一个地址,并且程序可访问该地址。比如变量,指针,类成员函数 ...
xvalue 具有一个地址,但程序不再能访问。
glvalue 具有地址
prvalue 它没有一个地址
rvalue 它没有一个地址,或者它有地址但是程序不再能访问

如果对这些术语有困惑,不必惊慌,随后会给出具体的示例。

3 值分类示例(Examples)

3.1 解释lvalue和prvalue

先看特别简单的表达式:

int y;
y = 10;   // 一个常见的表达式

先说结论:

  • 作为右操作数的数字10,,是ravlue, 更是prvalue.
  • 作为左操作数的变量y,是glvalue, 更是lvalue.

解释: 

  1. 右边的数字10是rvalue,因为它是没有内存地址的。更精确的说,它是prvalue(由于它是rvalue且不是xvalue)。
  2. 左边的变量 y是glvalue, 因为它是一个变量,它有内存地址。更精确的说,它是lvalue(由于这个变量不是临时变量, 程序可继续访问它的地址,意味着它不是xvalue)

3.1.1 赋值的左操作数需要lvalue

继续看示例。如果把上面的表达式反过来写呢?

10 = y;    // error!

不行,编译器会给出错误:

error: lvalue required as left operand of assignment
作为赋值的左操作数,需要lvalue。

如果我这样写呢?

int i = 2;
i * 4 = 7;  // error!

同样编译失败,因为i*4得到了一个prvalue。

如果我耍个滑头,试图用函数返回的prvalue作为左操作数呢?如下:

int setValue()
{
    return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!

编译器会给出同样的错误,想蒙混过关,欺骗编译器,那是不可能的!

下面这样写,完全就没有问题:

int my = 100;

int& set_my_value()
{
    return my;
}
// ... somewhere in main() ...
 set_my_value() = 400; // OK

3.1.2 不可以对prvalue取址

再继续做一些蠢操作,对数字使用&取地址:

int* y = &666; // error!

编译器给出错误:

lvalue required as unary ‘&’ operand
作为一元运算符&的操作数,必须是lvalue

因为666是prvalue,因此就不满足这个条件。

这样一来,经过这些示例,我们就解释了比较重要的lvalue和prvalue。后面如果能用“更精确的说”,那我们就尽可能用精确的值类别。这就无需关心glvalue和rvalue了。

那么xvalue呢? 欲知后事如何,请看下文分解。

3.1.3 更多的lvalue示例和左值引用

左值表达式的示例包括变量名,包括const变量,数组元素,位字段,union和类成员等等...我不打算给出全部的示例, 我想下面的示例已经足够了吧。

int i, j, *p;
i = 7;   // the variable i is lvalue
const j = 7;   //  the constant j is a non-modifiable lvalue

struct X { int n; };
X var_a;
var_a.n = 5;   // an class/strut member is lvalue

struct S {
 // three-bit unsigned field,
 // allowed values are 0...7
 unsigned int b : 3;
};
S s = {6};
++s.b; //  `s.b` bit field  is lvalue
...

我们注意的是,一种特殊的左值: lvalue reference(左值引用)

    int y = 10;
    int& yref = y;
    yref++;        // both y and yref now are 11;
    y++;        // both y and yref now are 12;

    const int& yref_2 = y;
    yref_2++;        //  error!  cannot modify through reference to const!
    y++;        // y ,yref, yref_2;   both three variables increase by 1

yref它是一个左值引用,yref_2是const修饰的左值引用。我们可以认为非const修饰的yrefy两个完全等价的别名。【Lvalue references can be used to alias an existing object (optionally with different cv-qualification)】

函数返回的左值引用:

int y = 10;
int& z = y;

int& fct_lvalue_ref(){
    z++;
    return z;
}

fct_lvalue_ref()++;   // now  z is 12

上面的例子只是告诉你,从C++语法的角度,左值引用的是可以这么写的。但是我不建议你在C++代码中使用这么古怪的写法。大多数时候,我们使用左值引用的最多的情况是--函数的传引用(准确说法是:传左值引用),这么做只是为了避免对象的拷贝:


struct Widget{int n;} ;
void foo(Widget& w){};   // Pass by rvalue reference
void c_foo(const Widget& w){};    // Pass by const rvalue reference

wrap up:
关于使用左值引用去得到别名,我不认为以这样的方式增加一个对象的别名具有显著的意义。可能在我们维护代码的时候有一些小小的帮助,尤其是对一个作用域比较广的全局变量,使用别名可以很好区分”新变动的代码“与”旧代码“之间的改动区别,容易识别代码的改动是基于哪个别名的。

3.2 解释xvalue

xvalue总是服务于C++的移动语义。在C++11之前,根本就没有xvalue的概念,就是因为引入移动语义导致了没办法解释新的C++表达式。

移动语义中的xvalue

void move_test(){
    std::string s = "I'm here!";
    std::string m  =  std::move(s);  // move from <s> to <m>
    // s is now in an undefined, but valid state;  S被清空了,但是S状态有效
    std::cout << "s=" << s << "; &s=" << &s << std::endl;
    std::cout << "m = " << m << "; &m=" << &m << std::endl;
}

运行结果:

s=; &s=0x7ffceba87480  // s被清了
m = I'm here!; &m=0x7ffceba874a0

std::move那条语句执行之后,注意s的值就清空了,但是它的内存地址还在。就是说未定义但状态有效。

这个表达式中的std::move(s)xvalue,因为std::move返回了一个右值引用。每个xvalue都是glvalue,也是rvalue。

我们应该注意的是,不是所有的右值引用都是xvalue,比如这种:

int&& rr_i = 7;    // rr_i is lvalue
原文地址:https://www.cnblogs.com/crb912/p/14471621.html