• C++ 11的右值引用


    C++11 引入了 std::move 语义、右值引用、移动构造和完美转发这些特性。由于这部分篇幅比较长,分为3篇来进行阐述。

    在了解这些特性之前,我们先来引入一些问题。

    一、问题导入

    1. 函数返回值是传值的时候发生几次对象构造、几次拷贝?
    2. 函数的形参是值传递的时候发生几次对象构造?

    让我们先来看一段代码,

    // main.cpp
    #include <iostream>
    using namespace  std;
    
    class A{
    public:
       A(){
          cout<<"class A construct!"<<endl;
       } 
       A(const A&){
          cout<<"class A copy!"<<endl;
       }
       A& operator=(const A&){
          cout<<"assignment called!"<<endl;
       }
       ~A(){
          cout<<"class A destruct!"<<endl;
       }
    };
    
    A get_A_value(){
        return A();
    }
    int main(){
        A a = get_A_value();
        return 0;
    }
    

    使用 g++ 编译;注意使用 -fno-elide-constructors关闭省略构造优化

    g++ main.cpp -fno-elide-constructors
    

    可以得到以下输出

    class A construct!
    class A copy!
    class A destruct!
    class A copy!
    class A destruct!
    class A destruct!
    

    可以看到A a=get_A_value(); 一行代码居然产生1次对象构造和2次对象的拷贝构造!具体为

    1. 在 get_A_value() 里 A() 构造了临时对象,发生了一次构造;
    2. 函数返回的时候会把临时对象拷贝后作为返回值,发生一次拷贝;
    3. A a = 函数返回值发生了拷贝构造。

    如果使用编译器优化(默认), 则会把临时对象拷贝的那次 和 用返回值构造最终对象的拷贝的给省略了;也即,只有一次构造和析构。

    class A construct!
    class A destruct!
    

    如果把上面代码改一下

    // ... A
    
    void pass_A_by_value(A a){
    
    }
    int main(){
        A a;
        pass_A_by_value(a);
        return 0;
    }
    

    在去掉优化 g++ main.cpp -fno-elide-constructors时输出为

    class A construct!
    class A copy!
    class A destruct!
    class A destruct!
    

    1次构造加上1次拷贝。

    因此,下次如果面试的时候有人问这个问题,你就可以说:默认情况下经过编译器优化后临时对象的拷贝就会被省去,如果使用 -fno-elide-constructors 省略优化,则还要考虑临时对象的拷贝。

    事实上,在未经优化的情况下,以下时候拷贝构造函数会被调用:

    1. 函数内的局部对象做为返回值返回(不是引用)的时候会发生拷贝(拷贝为临时对象返回)
    2. 函数形参为传值的时候,会发生拷贝构造
    3. 一个对象以另外一个对象进行初始化的时候

    对象的频繁构造是程序的开销,特别是当对象内部有堆上内存(比如有 new 出来的成员)的时候,每次拷贝构造的时候都需要用 new 申请一块内存,造成性能的降低。对于情况2,好习惯是如果函数参数是只读的(也即不会在程序内进行修改),传引用作为参数,也即 pass_A_by_refrence(const A &a); 对于情况1,编译器会为我们进行优化; 对于情况3,C++11 引入了一种移动构造函数的概念,它将获取**右值引用*,右值的“资源” move 到新对象中,这个过程中不会申请新的内存,从而达到提高了效率和性能。

    所以,要理解些关键词 “移动构造”、“移动语义” ,首先要理解右值和右值引用。

    二、右值和右值引用

    2.1 左值(lvalue)和右值(rvalue)

    在 一、问题导入 里我们提到了临时对象,也即函数返回值的时候只会“临时”存在的对象(运行超过那一行就会结束它的生存期),这个临时返回值就是一个右值;
    右值的最直观的定义为,顾名思义:

    位于赋值运算符 = 右边的值,为右值;在左边的则为左值

    A a = foo(); // foo() 为右值
    char *x = "thu"; // “thu”为字面值也为右值
    a = b + c; // b + c这个结果也是一个右值
    

    在C++中,还有个定义为:

    左值可以取得地址、有名字; 不可以取得地址、没有名字的为右值。

    所以 A a = foo()可以用 &a取得a的地址,a 是左值,然是不能取得 foo()的地址,(&foo())无法通过编译, foo()返回的临时对象也是没有名字的,所以是右值。

    在C++11中,右值包括两种,一中是将亡值(xvalue, eXpiring Value),一种是纯右值(prvalue,Pure Rvalue)[1]。函数非引用返回的临时对象、运算表达式的结果、1, 3.14,'c'这样的字面值等都属于纯右值。而xvalue则是由 C++11引入的 如返回值为 A&& 的函数返回值或者std::move()的返回值等。

    不深究的话,我们只需要知道左值和右值的区别就行了。对于右值的详细分类则不必深究。

    2.2 左值引用和右值引用

    1. 左值引用就是一般的引用,一般用一个&表示,例如
    const A &a_ref = a; // 取得对象 a 的引用
    

    左值引用相当于别名,指向一个具体的对象。

    1. 右值引用
      右值引用顾名思义,就是右值的引用, 用 &&表示;
    A &&r_ref = getRvalue(); // r_ref 是一个右值引用
    

    右值引用也相当于别名,与左值的区别为右值引用是无名变量的别名。

    getRvalue() 是一个返回右值的函数,右值在这一句执行完就该结束他的生存期了,如果是对象就该调用析构函数了;但是右值引用让它强行续命;使用右值引用指向右值,右值的生存期和右值引用一样长了,这也就少一次对象的析构和构造了。

    C++的右值引用主要有两个用处,一个是移动语义,一个是完美转发。这个将在接下来的两篇来讲。

    总结

    为了导入右值和移动语义,首先复习了以下临时对象在函数返回值和传参数时构造了几次;然后对比介绍了左值和右值,以及右值引用的形式和含义。为移动语义和完美转发的介绍做铺垫。

    参考资料

    1. Michale Wang| IBM XL 编译器中国 《深入理解C++11》, 机械工业出版社
  • 相关阅读:
    BZOJ3585&3339mex——主席树
    BZOJ1926[Sdoi2010]粟粟的书架——二分答案+主席树
    BZOJ2662[BeiJing wc2012]冻结——分层图最短路
    BZOJ1433[ZJOI2009]假期的宿舍——二分图最大匹配
    BZOJ1087[SCOI2005]互不侵犯——状压DP
    BZOJ4808马——二分图最大独立集
    BZOJ3175[Tjoi2013]攻击装置——二分图最大独立集
    BZOJ3524[Poi2014]Couriers——主席树
    BZOJ4010[HNOI2015]菜肴制作——拓扑排序+堆
    BZOJ2588Count on a tree——LCA+主席树
  • 原文地址:https://www.cnblogs.com/sunchaothu/p/11343517.html
Copyright © 2020-2023  润新知