• 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
    
  • 相关阅读:
    bzoj1007: [HNOI2008]水平可见直线(单调栈)
    1264: [AHOI2006]基因匹配Match(动态规划神题)
    bzoj1433: [ZJOI2009]假期的宿舍(最大二分图匹配)
    bzoj3931: [CQOI2015]网络吞吐量(spfa+网络流)
    [ZJOI2007]矩阵游戏
    [HAOI2007]覆盖问题
    [ZJOI2008]树的统计
    [ZJOI2010]数字计数
    [HAOI2006]旅行
    [HAOI2006]数字序列
  • 原文地址:https://www.cnblogs.com/crb912/p/14471621.html
Copyright © 2020-2023  润新知