c++11——右值引用

1. 左值和右值

    左值是表达式结束之后仍然存在的持久化对象,而右值是指表达式结束时就不再存在的临时对象。 
    c++11中,右值分为两种类型:将亡值(xvalue, expiring value),另一个是纯右值(prvalue, pure rvalue). 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值;将亡值是c++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值。

2. 左值引用

    左值引用是对左值进行引用的类型,分为常量左值引用和非常量左值引用。其中,常量左值引用可以引用常量左值、非常量左值、常量右值、非常量右值;而非常量左值引用只能引用非常量左值

    int x = 1;
    const int y = 2;
    int& p1 = x;
    int& p2 = y; //出错,非常量左值引用无法引用常量
    const int& p3 = x;
    const int& p4 = y;

 

3. 右值引用

    c++11增加了右值引用类型,实现对一个右值进行引用,标记为T&&. 因为右值不具名,因此只能通过引用的方式找到它。

右值引用延长右值的生命期 
    左值引用和右值引用必须在声明的时候立即初始化,因为引用本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期变得和右值引用类型变量的生命期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

利用右值引用延长右值生命期,可以避免一些临时对象的构造和析构,从而提高性能。比如:

class A{
public:
    A(){
        cout << "construct..." << endl;
    }
    ~A(){
        cout << "Destruct..." << endl;
    }
    A(const A&a a){
        cout << "Copy construct..." << endl;
    }
private:
};
A GetA(){
    return A();
}
int main(){
    A a = GetA();
    return 0;
}

 

以上代码,如果禁止编译器自动进行RVO优化,完全尊造c++的语法规则,则程序的输出为 
输出1

    其中,GetA()函数内部的A()函数生成一个内部的对象obj1时调用 构造函数; 在函数返回时,临时对象obj2通过拷贝构造函数拷贝了该内部对象的内容;函数返回之后,内部对象obj1调用析构函数销毁;然后 A a = obj2,通过调用拷贝构造函数,a拷贝obj2;a赋值结束之后,obj2 调用析构函数销毁;最后程序结束时,a调用析构函数销毁。

    这整个过程中,在调用函数GetA时会构造和析构临时对象obj2,因此造成不必要的浪费。在c++98/03中可以通过const A& a = GetA()来将临时对象obj2赋值给一个常量左值引用a,延长了该临时对象的生命期;而在c++11中,也可以通过右值引用来延长函数返回时的临时对象(右值)的生命期,而不是在a = GetA()一结束就销毁。 

    其中,GetA()函数内部的A()函数生成一个内部的对象obj1时调用 构造函数; 在函数返回时,临时对象obj2通过拷贝构造函数拷贝了该内部对象的内容;函数返回之后,内部对象obj1调用析构函数销毁;然后 const A& a = obj2或者 A&& a = obj2,此时都是对引用进行初始化,没有对象的构造和析构;当程序结束时,obj2(也就是a)进行析构。

  
  
 

4. T&&的赋值

(1)左值和右值是独立于他们的类别的,右值引用类型可能是左值也可能是右值 
(2)auto&& 或者函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal reference,它可能是左值引用类型也可能是右值引用类型,取决于初始化的值类型。 
(3)所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。当T&&为模板参数时,输入左值,它将会变成左值引用,而输入右值时则变为具名的右值引用。 
(4)编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

4.1 左值和右值是独立于他们的类别的,右值引用类型可能是左值也可能是右值 
    int && a = xxxx;中a本身的类型为右值引用,但它是一个具名的变量,为左值

    int &&var1 = 10; //var1为右值引用
    auto&& var2 = var1; //var2此时为一个universial reference,但是由于var1本身是一个左值,因此var2 为左值引用
    int w1, w2;
    auto&& v1 = w1; //左值引用
    decltype(w1)&& v2 = w2; //int &&v2 = w2; //此时对一个右值引用初始化为一个左值,出错!!

 

4.2 auto&& 或者函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal reference, 
它可能是左值引用类型也可能是右值引用类型,取决于初始化的值类型。

    auto&& a = 10; //a直接由一个右值初始化,则a为一个右值引用类型
    int x = 20;
    auto&& b = x; //b由一个左值进行初始化,则b为一个左值引用类型
    template<typename T>
    void func(T&& a){
    cout << a << endl;
    }
    ....
    func(10); //被一个右值初始化,a为右值引用类型, a类型为 int&&
    int x = 10;
    func(x); //左值引用类型, a为int& !!!!!
    void f(std:vector<T>&& param); //这里需要注意,这里既有推导类型T,又有确定类型vector。。。。在实际
    //应用时,调用该函数之前,Vector<T>中的推断类型T已经确定了,所以到调用该f函数的时候就没有类型推
    //则 param为右值引用
    template<typenmae T>
    void f(const T&& param){ //这里虽然有类型推导,但是由于带有const限定,则仍然为右值引用
    }

 

即右值操作符&&只在 auto && / T&& ,且不带cv限定符的时候,才需要推断具体为左值还是右值引用,否则一律为右值引用。

4.3 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。 
当T&&为模板参数时,输入左值,它将会变成左值引用,而输入右值时则变为具名的右值引用。

引用折叠 
    由于存在T&&这种未定引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化成为引用折叠。

    typedef const int T;
    typedef T& TR;
    TR v

 

TR的定义v的定义v的实际类型
T& TR v T&
T& TR& v T&
T& TR&& v T&
T&& TR v T&&
T&& TR& v T&
T&& TR&& v T&&

从而,可以看出 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。

4.4 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

    void print(int& i){
        cout << "lvalue " << i << endl;
    }
    void print(int&& i){
        cout << "rvalue " << i << endl;
    }
    void forward(int&& i){
        print(i);
    }
    forward(10); //10为右值,进入forward之后,10变为i,i为一个变量,变为左值。
    //因此,输出 "lvalue " << i << endl;

 

5. 右值引用优化性能,避免深拷贝

    对于含有堆内存的类,需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,将会导致堆内存的重复删除。

class A{
public:
    A(): m_ptr(new int(0)){};
    ~A(){
        delete m_ptr;
    }
private:
    int *m_ptr;
};
A Get(bool flag){
    A a;
    A b;
    if (flag)
        return a;
    else
        return b;
}
int main(){
    A a = Get(false);   //默认的拷贝构造函数,只是简单的将m_ptr进行赋值
    //在 Get函数内部的b被析构的时候delete m_ptr, 当程序结束的时候a析构也delete m_ptr,二者m_ptr相同。造成内存的重复析构
    return 0;
}

 

    而如果为类的拷贝构造函数提供了深拷贝,则在程序产生临时对象的时候会出现大量的内存拷贝,降低性能。此时可以使用移动构造函数进行改进。

class A{
public:
    A(): m_ptr(new int(0)){};
    A(const A& a):m_ptr(new int(*a.m_ptr)){}; //深拷贝的拷贝构造函数
    A(A&& a):m_ptr(a.m_ptr){    //移动构造函数
        a.m_ptr = NULL;
    }
    
    ~A(){
        delete m_ptr;
    }
private:
    int *m_ptr;
};
A Get(bool flag){
    A a;
    A b;
    if (flag)
        return a;
    else
        return b;
}
int main(){
    A a = Get(true); //调用移动构造函数
    A b = a;         //调用拷贝构造函数
    return 0;
}

 

    使用移动构造函数,其参数是一个右值引用类型的参数 A&&, 没有深拷贝,只有浅拷贝,避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数;否则,则会选择拷贝构造函数 
    在拷贝的源对象为临时的时候,调用移动构造函数,该函数将原来临时对象的资源移动到了拷贝的目的对象,并且将源对象的资源赋空。则之后,源对象被析构,资源已经被转移到目的对象。

    除了使用移动构造补充拷贝构造,还可以使用移动赋值操作符代替拷贝赋值操作符。

class A{
public:
    A(): m_ptr(new int(0)){};
    A(const A& a):m_ptr(new int(*a.m_ptr)){}; //深拷贝的拷贝构造函数
    A(A&& a):m_ptr(a.m_ptr){    //移动构造函数
        a.m_ptr = NULL;
    }
    A& operator=(const A& a){
        m_ptr = new int(*a.m_ptr);
    }
    A& operator=(A&& a){
        m_ptr = a.m_ptr;
        a.m_ptr = NULL;
    }
    ~A(){
        delete m_ptr;
    }
private:
    int *m_ptr;
};
A Get(bool flag){
    A a;
    A b;
    if (flag)
        return a;
    else
        return b;
}
int main(){
    A a = Get(true); //调用移动构造函数
    A b = a;         //调用拷贝构造函数
    
    a = Get(false); //调用移动赋值操作符
    a = b;  //调用拷贝赋值操作符
    return 0;
}

 


    上面添加了move版本的构造函数和赋值函数,对原来的类产生了一些影响: 如果提供了move版本的构造函数,则不会生成默认的构造函数。另外,编译器永远不会自动生成move版本的构造函数和赋值函数,他们需要手动显式的添加。 
    当添加了move版本的构造函数和赋值函数的重载形式后,某一个函数调用应当使用哪一个重载版本呢?下面是按照判决的优先级列出的3条规则: 
(1)常量值只能绑定到常量引用上,不能绑定到非常量引用上 
(2)左值优先绑定到左值引用上,右值优先绑定到右值引用上 
(3)非常量值优先绑定到非常量引用上

 

c++类的拷贝构造函数和赋值操作符:

class A{
public:
  A(int x){
    x_ = x;

  };
  A(const A& a){
    x_ = a.x_;
  }
     A& operator=  (const A& a){
    x_ = a.x_;
  }
private:
  int x_;
};
A  getA(int x){
  return A(x);
} ;
A a = getA(1);  //拷贝构造函数
A b = getA(2); //拷贝构造函数 
b = a;    //赋值操作符

拷贝构造函数是在构造类的对象的时候调用的,即 A a = getA(1); 这句新建了一个类A的对象a,调用拷贝构造函数。而 赋值操作符是对一个已经存在的对象进行重新赋值, 不重新生成对象。

 

参考

http://www.cnblogs.com/hujian/archive/2012/02/13/2348621.html

+
posted @ 2015-09-13 21:01  农民伯伯-Coding  阅读(996)  评论(0编辑  收藏  举报