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++的语法规则,则程序的输出为
其中,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