何为右值?
下面两行代码中i是左值,10是右值;v是左值,getVar()返回的是个临时变量,也是右值,改临时变量在改行表达式结束后消失。
1 int i = 10; 2 int &j = getVar(); 3 auto f = []{return 6;};
左值的特征,我的理解是能取地址的是左值,不能取指,生存周期仅限于某个表达式的是右值,如上面的10,我们是取不到地址的,因为该行表达式结束后变量即被释放,即使取址了也没有意义,后面没法利用这个地址在操作这个值了。右值是无名临时变量。
C++中函数返回的非引用临时变量、lamda表达式、运算表达式产生的临时变量都属于纯右值。
Why右值引用?
就是对右值的引用咯。废话。。。写法是T &&a=b;
1 int&& i = 10; //i是对10的右值引用
C++11为何要有右值引用呢?为了解决C++98的临时变量的拷贝问题,OBJ的拷贝开销是比较大的。
很多时候有这样一个场景:一个函数要返回一个对象,或对象的引用,但这个对象是临时的,因此就不能返回引用了,只能返回OBJ,对临时变量进行拷贝,如下所示:
1 T getObj(void) 2 { 3 T OBJ; 4 //do sth with OBJ 5 return OBJ; 6 }
右值引用很好的解决了这个问题。比较下面两行代码:
1 T obj= getObj(); //调用了T的拷贝构造函数,成本高 2 T &&objk= getObj(); //对返回对象的右值引用
上面这两行代码的唯一差别就在于前面的&&,但是语意上相差很大:第一行是熟悉的,会调用拷贝构造函数,而第二行是对返回的临时对象的右值拷贝,会对临时对象进行续命,这个临时对象和objk的声明周期是一致的。用了右值引用就减少了一次临时变量的拷贝(拷贝给返回值),一次临时变量的析构。
T&&一定是右值吗?
看如下一段代码:
1 void post(int &&t) {} 2 3 int main() 4 { 5 post(10); //OK 6 7 int &&x = 10; 8 post(x); //Error, x是左值,需要传右值。 9 }
上述代码确实让人认为只能传右值给post函数作为入参。但是当发生自动推导,也就是模版或者auto时,情况就不一样了:
1 template <typename T> 2 void post(T &&t) {} 3 4 int main() 5 { 6 post(10); //OK 7 8 int &&x = 10; 9 post(x); //OK 10 }
T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references)(个人认为是左值引用或者右值引用类型,虽然写了T&&),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。
Why右值引用2.0
有这样一个场景,某个函数返回临时变量OBJ,按照C++98的做法是调用拷贝构造函数,注意:如果OBJ内有指针,肯定是要深拷贝的,也就是说要重新new出一个堆来。但是对临时变量的深拷贝完成后,马上就释放了堆内存,特别是对于内存占用比较大的OBJ,不仅多余,而且严重影响性能。C++11的右值引用很好的解决了这个问题。C++11为了解决这个问题,提出了move constructor的概念,也就是移动构造函数。
1 class A 2 { 3 public: 4 A() :m_ptr(new int(0)){} 5 A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数 6 { 7 cout << "copy construct" << endl; 8 } 9 A(A&& a) :m_ptr(a.m_ptr) // move constructor 10 { 11 a.m_ptr = nullptr; 12 cout << "move construct" << endl; 13 } 14 ~A(){ delete m_ptr;} 15 private: 16 int* m_ptr; 17 }; 18 19 A GetA() 20 { 21 return A(); 22 } 23 24 int main() 25 { 26 A a = GetA(); 27 }
上面的代码并没有调用拷贝构造函数进行深拷贝,而是调用了move constructor,简单的将指针的所有权交给了新的对象,之前的指针变为null。
1 A(A&& a) :m_ptr(a.m_ptr) 2 { 3 a.m_ptr = nullptr; 4 cout << "move construct" << endl; 5 }
这里怎么知道要调用move constructor而不是copy constructor的呢?这里并没发生类型推导,因为都是明确的类型。所以入参必须是右值,否则编译报错。函数返回值属于右值,所有调用了move constructor了。试问:左值也想要使用move constructor呢?C++11提供了std::move,将左值转换为右值,便于使用move constructor。需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。
std::move
1 { 2 std::list< std::string> tokens; 3 //省略初始化... 4 std::list< std::string> t = tokens; //这里存在拷贝 5 } 6 std::list< std::string> tokens; 7 std::list< std::string> t = std::move(tokens); //这里没有拷贝
如果不用std::move,拷贝的代价很大,性能较低。std::move避免了不必要的深拷贝。
std::move只是将左值左了cast_to_rvalue操作然后显式的调用移动构造函数,对于右值,直接使用std::move显式调用移动构造函数。并不是所有情况用std::move都好的,比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝,因为他没有对应的移动构造函数。对于含有句柄或是指针的对象(也就是需要深拷贝的对象),std::move操作更有意义。说到这里,个人认为右值引用的根本目的就是减少不必要的深拷贝,而不是避免拷贝。对于浅拷贝,右值引用意义不大。
std::forward<T>
之前说到过,模版自动推导时或者auto时T&&接收的参数不一定时右值,也有可能时右值,原本型参是左值的,传进来后也变成右值了。如下实验:
void proccessValue(int &i) { cout<<"rvalue"<<endl; } void proccessValue(int &&i) { cout<<"lvalue"<<endl; } template <typename T> void forwardValue(T &&i) { proccessValue(i); //i变成了右值,不管传进来之前是左还是右 } int main() { int i = 10; forwardValue(10); forwardValue(i); }
这就造成了后续使用的困惑了。为了保持型参原来的左右属性,使用std::forward<T>
1 template <typename T> 2 void forwardValue(T &&i) 3 { 4 5 proccessValue(std::forward<T>(i)); 6 }
overall
右值引用解决了无谓的拷贝构造函数的开销。
移动构造函数(move constructor)告诉编译器如何进行拷贝右值对象,避免了深拷贝的开销。
左值也想使用右值引用的特性,所有C++11提供了std::move函数,将左值cast_to_rvalue了。
类型推导时传给T&&后导致左值变成了右值,此时想利用其原始的左值属性的话,C++11又提供了std::forward<T>()。
本文也参考了:从4行代码看右值引用