探索C++0x: 3. 右值引用(rvalue reference)(本文前段还行)

转载请注明来源: http://blog.csdn.net/thesys/archive/2010/06/06/5651713.aspx

简介

C++0x 中引入了右值引用(rvalue reference)这个设施,形如T&&,用来实现移动语义(move semantics)和完美转发(perfect forwarding)。此前C++中有一个著名的性能问题——复制临时对象,由于右值引用的引入,该问题将得到极大的改善。

虽然右值引用的引入是一个很了不起的进步,也是一个明智的决定,但它并不那么讨人喜欢,至少我觉得如此。原因有二:首先是其概念本身就不容易理解,增加了一些智力负担;另外如果想享受它带来的性能好处,还必须增加一些编码工作量。

什么是左值和右值

我们首先需要熟悉一下现行C++98/03标准中,左值和右值的概念。网上现在充斥着很多对它们的解释,长篇大论的有,只言片语的也有,而且不一定清晰正确,可谓良莠不齐,这里我希望能够尽量简单准确地进行说明。

这里有两个问题经常令人混淆,必须首先澄清一下:

1. lvalue(left value)和rvalue(right value)的概念经过了长时间的演化,早已名不副实,千万不要把L和R当做真正的左右来理解。最早或许left value意味着在等号左边,right value意味着在等号右边,但对于现代C++这么复杂的语法来说,早就不适用了。
2. 左值还是右值,是针对某个表达式而言的,而不是针对某个具体值或者对象本身而言。C++ 03 标准 3.10/1 节:“每一个表达式要么是一个lvalue,要么就是一个rvalue。”因此不要单纯的考虑数字1是左值还是右值,或者存在地址0x100000上的那 个string对象到底是左值还是右值,重要的是看表达式。

左值和右值的定义和判断:

如果一个语句结束的时候,该表达式代表的对象立刻被销毁,则为右值,否则就是左值。也就是说右值代表的是临时对象或者字面值,而左值则不是临时对象。引申出来的另一个判断方法是:具名的表达式意味着是左值,非具名的则为右值(非具名左值引用是个例外,它是左值)。 示例代码如下:

view plain copy to clipboard print ?
  1. int  add( int  v1,  int  v2)  
  2. {  
  3.     return  v1 + v2;  
  4. }  
  5.   
  6. int & g()  
  7. {  
  8.     static   int  i = 100;  
  9.     return  i;  
  10. }  
  11.   
  12. void  example()  
  13. {  
  14.     int  x = add(1, 2);   // x是左值,add(1, 2)的返回值是右值,x拷贝了它。   
  15.                         // x是持久的,具名的;   
  16.                         // add(1, 2)的返回值是临时的,非具名的。   
  17.                         // 另外这里字面值1和2也都是右值,临时的,非具名的。   
  18.     ++x;                // ++x是左值,它表示的x本身,是持久的,具名的   
  19.     x++;                // x++是右值,它表示x的原值,是临时的,非具名的   
  20.     g();                // g()是左值,虽然返回值是非具名的,但是左值引用   
  21. }  
int add(int v1, int v2) { return v1 + v2; } int& g() { static int i = 100; return i; } void example() { int x = add(1, 2); // x是左值,add(1, 2)的返回值是右值,x拷贝了它。 // x是持久的,具名的; // add(1, 2)的返回值是临时的,非具名的。 // 另外这里字面值1和2也都是右值,临时的,非具名的。 ++x; // ++x是左值,它表示的x本身,是持久的,具名的 x++; // x++是右值,它表示x的原值,是临时的,非具名的 g(); // g()是左值,虽然返回值是非具名的,但是左值引用 }

现行C++标准对右值的限制:

由于临时对象在语句结束后被立刻销毁,因此在语句结束后还使用它是不安全的。 所以现行C++标准中,规定右值是不能被具名引用的,因为一旦被引用了就可能被使用。但令人不爽的是,由于函数在传递参数时,又需要让临时对象可以被作为 实参传递,C++标准只好又规定右值可以被具名引用,但只能被常量引用,而不能被被非常量引用,且被常量引用时,如果该常量引用是具名的(也就是左值), 则该临时对象的生命周期延长到和该常量引用相同。其实这个规定挺搞笑的,完全可以不用区分是否是常量,同样规定右值也可以被非常量引用,不过标准既然这么 说了,那编译器就只好这么办(其实VC的某些版本没这么干)。这里要注意的是,左值一旦被具名引用,则变成了右值。示例代码如下:

view plain copy to clipboard print ?
  1. int  add( int  v1,  int  v2)  
  2. {  
  3.     return  v1 + v2;  
  4. }  
  5.   
  6. void  example()  
  7. {  
  8.     int   const & x1 = add(1, 2);   // 正确,按照C++现行标准,右值可以被绑定到常量引用   
  9.                                 // 不过一旦绑定到具名引用,则成为左值,这里x1就是左值   
  10. #if _SHOW_ERROR_CASE   
  11.     int & y1 = add(3, 4);         // 错误,按照C++现行标准,右值不能被绑定到非常量引用   
  12. #endif   
  13. }  
int add(int v1, int v2) { return v1 + v2; } void example() { int const& x1 = add(1, 2); // 正确,按照C++现行标准,右值可以被绑定到常量引用 // 不过一旦绑定到具名引用,则成为左值,这里x1就是左值 #if _SHOW_ERROR_CASE int& y1 = add(3, 4); // 错误,按照C++现行标准,右值不能被绑定到非常量引用 #endif }

为什么要引入右值引用?

了解临时对象造成的性能问题,了解RVO

在现行的C++标准中,如果函数返回一个对象,则该对象是一个临时对象,也就 是一个右值。这就带来一个很头痛的性能问题,就是该对象会被白白的拷贝好几遍。这种纯粹浪费性能的行为完全违背了C++的设计哲学,因此现行C++标准 中,不惜破坏原来简单的拷贝语义,提出可以在拷贝构造的情况下省略掉多余的拷贝,这就是著名的RVO(Return Value Optimization)。但是很多时候,RVO都不能完全奏效,性能浪费依然不能避免。示例代码如下:

view plain copy to clipboard print ?
  1. // 一个简单的整型数组类,仅供示例,并非最佳设计   
  2. class  int_array  
  3. {  
  4. public :  
  5.   
  6.     ~int_array()    // 析构函数   
  7.     {  
  8.         cout << "  int_array::~int_array()"  << endl;  
  9.         free_memory();  
  10.     }  
  11.   
  12.     int_array() // 默认构造函数   
  13.     {  
  14.         cout << "  int_array::int_array()"  << endl;  
  15.         alloc_memory(0);  
  16.     }  
  17.   
  18.     int_array(int_array const & src)  // 拷贝构造函数   
  19.     {  
  20.         cout << "  int_array::int_array(int_array const&)"  << endl;  
  21.         alloc_memory(src.m_size);  
  22.         memcpy(m_buffer, src.m_buffer, m_size * sizeof ( int ));  
  23.     }  
  24.   
  25.     int_array(unsigned int  size)    // 用来初始化为0值的构造函数   
  26.     {  
  27.         cout << "  int_array::int_array(unsigned int)"  << endl;  
  28.         alloc_memory(size);  
  29.         memset(m_buffer, 0, size * sizeof ( int ));  
  30.     }  
  31.   
  32.     int_array& operator=(int_array const & rhs)   // 赋值操作符   
  33.     {  
  34.         cout << "  int_array& int_array::operator=(int_array const&)"  << endl;  
  35.         if  ( this  != &rhs)  
  36.         {  
  37.             free_memory();  
  38.             alloc_memory(rhs.m_size);  
  39.             memcpy(m_buffer, rhs.m_buffer, m_size * sizeof ( int ));  
  40.         }  
  41.         return  * this ;  
  42.     }  
  43.   
  44.     unsigned int  size()  const     // 获取大小   
  45.     {  
  46.         return  m_size;  
  47.     }  
  48.   
  49.     int  get_at(unsigned  int  offset)  const     // 获取指定位置的值   
  50.     {  
  51.         return  m_buffer[offset];  
  52.     }  
  53.   
  54.     void  set_at(unsigned  int  offset,  int  value)  // 设置指定位置的值   
  55.     {  
  56.         m_buffer[offset] = value;  
  57.     }  
  58.   
  59. private :  
  60.   
  61.     void  alloc_memory(unsigned  int  size)     // 分配内存   
  62.     {  
  63.         cout << "    int_array::alloc_memory(unsinged int)"  << endl;  
  64.         m_buffer = new   int [size];  
  65.         m_size = size;  
  66.     }  
  67.   
  68.     void  free_memory()   // 释放内存   
  69.     {  
  70.         cout << "    int_array::free_memory()"  << endl;  
  71.         delete [] m_buffer;  
  72.     }  
  73.   
  74.     int * m_buffer;  
  75.     unsigned int  m_size;  
  76. };  
  77.   
  78. // 生成一个有两个元素的int_arry   
  79. int_array make_int_array2(int  v1,  int  v2)  
  80. {  
  81.     int_array result(2);    // 调用构造函数   
  82.     result.set_at(0, v1);  
  83.     result.set_at(1, v2);  
  84.     return  result;           // 调用拷贝构造函数,但可以被RVO   
  85. }  
  86.   
  87. // 生成一个有size个元素的int_arry   
  88. int_array make_int_array(unsigned int  size, ...)  
  89. {  
  90.     if  (size > 10000000)     // 如果太大则返回一个空的   
  91.         return  int_array();  // 调用构造函数和拷贝构造函数   
  92.   
  93.     int_array result(size); // 调用构造函数   
  94.     va_list  args;  
  95.     va_start(args, size);  
  96.     for  (unsigned  int  i = 0; i < size; ++i)  
  97.     {  
  98.         int  v = va_arg(args,  int );  
  99.         result.set_at(i, v);  
  100.     }  
  101.     return  result;           // 调用拷贝构造函数,   
  102.                             // 两个不同的return导致RVO失败   
  103. }  
  104.   
  105. void  example()  
  106. {  
  107.     cout << endl << "enter"  << endl;  
  108.   
  109.     cout << endl << "step1"  << endl;  
  110.   
  111.     // 可以RVO,仅调用一次构造函数,否则按照原始语义,应该有三次构造   
  112.     int_array ia1 = make_int_array2(10, 20);  
  113.   
  114.     cout << endl << "step2"  << endl;  
  115.   
  116.     // 无法完全RVO,调用一次构造函数,一次赋值操作,浪费一次内存分配和释放   
  117.     ia1 = make_int_array2(100, 200);  
  118.   
  119.     cout << endl << "step3"  << endl;  
  120.   
  121.     // 无法完全RVO,调用两次构造函数,浪费一次内存分配和释放   
  122.     int_array ia2 = make_int_array(5, 1, 2, 3, 4, 5);  
  123.   
  124.     cout << endl << "step4"  << endl;  
  125.   
  126.     // 无法RVO,调用两次次构造函数,一次赋值操作,浪费两次内存分配和释放   
  127.     ia2 = make_int_array(3, 1, 2, 3);  
  128.   
  129.     cout << endl << "exit"  << endl;  
  130. }  
// 一个简单的整型数组类,仅供示例,并非最佳设计 class int_array { public: ~int_array() // 析构函数 { cout << " int_array::~int_array()" << endl; free_memory(); } int_array() // 默认构造函数 { cout << " int_array::int_array()" << endl; alloc_memory(0); } int_array(int_array const& src) // 拷贝构造函数 { cout << " int_array::int_array(int_array const&)" << endl; alloc_memory(src.m_size); memcpy(m_buffer, src.m_buffer, m_size * sizeof(int)); } int_array(unsigned int size) // 用来初始化为0值的构造函数 { cout << " int_array::int_array(unsigned int)" << endl; alloc_memory(size); memset(m_buffer, 0, size * sizeof(int)); } int_array& operator=(int_array const& rhs) // 赋值操作符 { cout << " int_array& int_array::operator=(int_array const&)" << endl; if (this != &rhs) { free_memory(); alloc_memory(rhs.m_size); memcpy(m_buffer, rhs.m_buffer, m_size * sizeof(int)); } return *this; } unsigned int size() const // 获取大小 { return m_size; } int get_at(unsigned int offset) const // 获取指定位置的值 { return m_buffer[offset]; } void set_at(unsigned int offset, int value) // 设置指定位置的值 { m_buffer[offset] = value; } private: void alloc_memory(unsigned int size) // 分配内存 { cout << " int_array::alloc_memory(unsinged int)" << endl; m_buffer = new int[size]; m_size = size; } void free_memory() // 释放内存 { cout << " int_array::free_memory()" << endl; delete[] m_buffer; } int* m_buffer; unsigned int m_size; }; // 生成一个有两个元素的int_arry int_array make_int_array2(int v1, int v2) { int_array result(2); // 调用构造函数 result.set_at(0, v1); result.set_at(1, v2); return result; // 调用拷贝构造函数,但可以被RVO } // 生成一个有size个元素的int_arry int_array make_int_array(unsigned int size, ...) { if (size > 10000000) // 如果太大则返回一个空的 return int_array(); // 调用构造函数和拷贝构造函数 int_array result(size); // 调用构造函数 va_list args; va_start(args, size); for (unsigned int i = 0; i < size; ++i) { int v = va_arg(args, int); result.set_at(i, v); } return result; // 调用拷贝构造函数, // 两个不同的return导致RVO失败 } void example() { cout << endl << "enter" << endl; cout << endl << "step1" << endl; // 可以RVO,仅调用一次构造函数,否则按照原始语义,应该有三次构造 int_array ia1 = make_int_array2(10, 20); cout << endl << "step2" << endl; // 无法完全RVO,调用一次构造函数,一次赋值操作,浪费一次内存分配和释放 ia1 = make_int_array2(100, 200); cout << endl << "step3" << endl; // 无法完全RVO,调用两次构造函数,浪费一次内存分配和释放 int_array ia2 = make_int_array(5, 1, 2, 3, 4, 5); cout << endl << "step4" << endl; // 无法RVO,调用两次次构造函数,一次赋值操作,浪费两次内存分配和释放 ia2 = make_int_array(3, 1, 2, 3); cout << endl << "exit" << endl; }

右值引用是救星:

为了解决这个问题,有一个很直观的方案,就是对临时对象的内部的数据(如上例中的m_buffer成员)进行操作,将它们“移动”或者“交换”过来,从而取代拷贝行为,因为反正临时对象马上就要销毁的,移动过来岂不正好?

但问题来了,如果要对临时对象的内部数据进行修改,至少需要具备两个条件:一个是需要引用临时对象,否则怎么有机会修改它呢;另一个是要识别对象是不是一个临时对象,改错了可就完蛋了。

如果简单的规定右值可以被引用,而且可以被非常量的引用,貌似可以解决第一个问题,但第二个问题还是没法解决,因为你不知道一个非常量的引用到底是不是临时对象。

因此C++0x标准中引入了右值引用,用两个连续的“&”符号来表 示,和左值引用以示区别。例如int&&就表示一个整型的右值引用,而int&则还是和原来一样,表示整型的左值引用。 C++0x标准进一步规定,除了原来规定的右值可以绑定到常量左值引用外,右值还可以绑定到右值引用,当然一旦被具名引用,右值还是会变成左值。而且遇到 重载时,优先考虑将右值绑定到右值引用而不是左值引用(那当然,否则这玩意儿就废了)。另外,左值不允许绑定到右值引用,除非强制类型转换,这一点也很重 要,以免无意中将非临时对象的内部数据给移走了。示例代码如下:

view plain copy to clipboard print ?
  1. int  add( int  v1,  int  v2)  
  2. {  
  3.     return  v1 + v2;  
  4. }  
  5.   
  6. int   const  add_const( int  v1,  int  v2)  
  7. {  
  8.     return  v1 + v2;  
  9. }  
  10.   
  11. void  func( int & i)  
  12. {  
  13.     cout << "func(int&)"  << endl;  
  14. }  
  15.   
  16. void  func( int   const & i)  
  17. {  
  18.     cout << "func(int const&)"  << endl;  
  19. }  
  20.   
  21. void  func( int && i)  
  22. {  
  23.     cout << "func(int&&)"  << endl;  
  24. }  
  25.   
  26. void  func( int   const && i)  
  27. {  
  28.     cout << "func(int const&&)"  << endl;  
  29. }  
  30.   
  31. template  < typename  T>  
  32. void  f1(T&&)  
  33. {  
  34. }  
  35.   
  36. void  example()  
  37. {  
  38.     int  x1 = 1;                      // 正确,右值可以拷贝到左值   
  39.     int & x2 = x1;                    // 正确,左值可以绑定到左值引用   
  40.     int   const & x3 = x1;              // 正确,非常量左值可以绑定到常量左值引用   
  41.     int   const & x4 = add(1, 2);       // 正确,右值可以被绑定到常量左值引用   
  42.     int && x5 = add(1, 2);            // 正确,右值可以被绑定到右值引用   
  43.     int   const && x6 = add_const(1, 2);  // 正确,常量右值可以被绑定到常量右值引用   
  44.     int   const && x7 = add(1, 2);      // 正确,非常量右值可以被绑定到常量右值引用   
  45.     int & x8 = x5;                    // 正确,左值可以绑定到左值引用,x5虽然是右值引用   
  46.                                     // 但由于已经是具名引用,因此变成了左值   
  47.   
  48.     func(x1);                       // 调用func(int&)   
  49.     func(x3);                       // 调用func(int const&)   
  50.     func(add(1, 2));                // 调用func(int&&)   
  51.     func(add_const(1, 2));          // 应调用func(int const&&),add_const是常量右值,   
  52.                                     // 但GCC4.5有bug,调用func(int&&),VC10是正确的   
  53.     func(1);                        // 调用func(int&&), 字面值1被当做非常量右值   
  54.     func(x5);                       // 调用func(int&),x5虽然是右值引用   
  55.                                     // 但由于已经是具名引用,因此变成了左值   
  56.     f1(x1);                         // 对于函数模板的推导,C++0x引入了reference collapsing,   
  57.                                     // 并对左值的推导进行了特别规定,是因为要实现完美转发   
  58.                                     // 因此这里看起来好像是左值被绑定到右值引用,   
  59.                                     // 但实质上还是左值引用   
  60. #if _SHOW_ERROR_CASE   
  61.     int & y1 = add(1, 2);             // 错误,右值不能绑定到非常量左值引用   
  62.     int & y2 = x3;                    // 错误,常量左值不能绑定到非常量左值引用   
  63.     int && y3 = x1;                   // 错误,左值不能绑定到右值引用   
  64.     int && y4 = add_const(1, 2);      // 错误,常量右值不能绑定到非常量右值引用   
  65.     int   const && y5 = x1;             // 错误,左值不能绑定到右值引用,常量右值引用也不行   
  66.     int && y6 = x5;                   // 错误,左值不能绑定到右值引用,x5虽然是右值引用,   
  67.                                     // 但由于已经是具名引用,因此变成了左值   
  68. #endif   
  69. }  
int add(int v1, int v2) { return v1 + v2; } int const add_const(int v1, int v2) { return v1 + v2; } void func(int& i) { cout << "func(int&)" << endl; } void func(int const& i) { cout << "func(int const&)" << endl; } void func(int&& i) { cout << "func(int&&)" << endl; } void func(int const&& i) { cout << "func(int const&&)" << endl; } template <typename T> void f1(T&&) { } void example() { int x1 = 1; // 正确,右值可以拷贝到左值 int& x2 = x1; // 正确,左值可以绑定到左值引用 int const& x3 = x1; // 正确,非常量左值可以绑定到常量左值引用 int const& x4 = add(1, 2); // 正确,右值可以被绑定到常量左值引用 int&& x5 = add(1, 2); // 正确,右值可以被绑定到右值引用 int const&& x6 = add_const(1, 2); // 正确,常量右值可以被绑定到常量右值引用 int const&& x7 = add(1, 2); // 正确,非常量右值可以被绑定到常量右值引用 int& x8 = x5; // 正确,左值可以绑定到左值引用,x5虽然是右值引用 // 但由于已经是具名引用,因此变成了左值 func(x1); // 调用func(int&) func(x3); // 调用func(int const&) func(add(1, 2)); // 调用func(int&&) func(add_const(1, 2)); // 应调用func(int const&&),add_const是常量右值, // 但GCC4.5有bug,调用func(int&&),VC10是正确的 func(1); // 调用func(int&&), 字面值1被当做非常量右值 func(x5); // 调用func(int&),x5虽然是右值引用 // 但由于已经是具名引用,因此变成了左值 f1(x1); // 对于函数模板的推导,C++0x引入了reference collapsing, // 并对左值的推导进行了特别规定,是因为要实现完美转发 // 因此这里看起来好像是左值被绑定到右值引用, // 但实质上还是左值引用 #if _SHOW_ERROR_CASE int& y1 = add(1, 2); // 错误,右值不能绑定到非常量左值引用 int& y2 = x3; // 错误,常量左值不能绑定到非常量左值引用 int&& y3 = x1; // 错误,左值不能绑定到右值引用 int&& y4 = add_const(1, 2); // 错误,常量右值不能绑定到非常量右值引用 int const&& y5 = x1; // 错误,左值不能绑定到右值引用,常量右值引用也不行 int&& y6 = x5; // 错误,左值不能绑定到右值引用,x5虽然是右值引用, // 但由于已经是具名引用,因此变成了左值 #endif }

有了这样一个规定,就简单了,首先右值可以被引用了,其次为了分辨对象是不是 临时的,我们可以做两个重载的函数,其中一个的形参是左值引用,另一个的形参是右值引用,那么该函数被调用时,如果参数是左值(非临时对象)或者左值引 用,编译器会自动调用前一个重载的函数,如果参数是右值(临时对象)或者右值引用,则会自动调用后一个重载的函数,这样我们就可以准确的对左值和右值分开 处理了。根据此原理,我们对前面例子中的int_array类进行修改,增加一个拷贝构造函数重载,和一个赋值操作符重载,用来接受右值引用,并修改使用 的地方。示例代码如下:

view plain copy to clipboard print ?
  1. // 拷贝构造函数重载,其实是转移构造函数,接受非常量右值,用于转移内部数据,   
  2. // 对于常量的右值,由于不能修改内容,还是需要拷贝,正好走传统的拷贝构造函数   
  3. int_array(int_array&& src)  
  4. {  
  5.     cout << "  int_array::int_array(int_array&&)"  << endl;  
  6.     // 直接转移内部数据,避免了额外的拷贝   
  7.     m_buffer = src.m_buffer;  
  8.     m_size = src.m_size;  
  9.     src.m_buffer = 0;  
  10.     src.m_size = 0;  
  11. }  
  12.   
  13. // 赋值操作符重载,其实可以看做交换操作符,接受非常量右值,用于交换内部数据   
  14. // 对于常量右值,由于无法修改其内部数据,还是需要拷贝,正好走传统的赋值操作符   
  15. int_array& operator=(int_array&& rhs)  
  16. {  
  17.     cout << "  int_array& int_array::operator=(int_array&&)"  << endl;  
  18.     if  ( this  != &rhs)  
  19.     {  
  20.         // 交换内部数据,这样等下销毁的就是这个对象现在的内部数据了,   
  21.         // 从而避免了不必要的拷贝   
  22.         swap(m_buffer, rhs.m_buffer);  
  23.         swap(m_size, rhs.m_size);  
  24.     }  
  25.     return  * this ;  
  26. }  
  27.   
  28. // 生成一个有两个元素的int_arry,采用了转移语义减少拷贝   
  29. int_array new_make_int_array2(int  v1,  int  v2)  
  30. {  
  31.     int_array result(2);    // 调用构造函数   
  32.     result.set_at(0, v1);  
  33.     result.set_at(1, v2);  
  34.   
  35.     // 通过move函数将result对象从左值转成右值,   
  36.     // 从而调用int_array的转移构造函数,以减少不必要的拷贝   
  37.     return  int_array(std::move(result));  
  38. }  
  39.   
  40. // 生成一个有size个元素的int_arry,采用了转移语义减少拷贝   
  41. int_array new_make_int_array(unsigned int  size, ...)  
  42. {  
  43.     if  (size > 10000000)     // 如果太大则返回一个空的   
  44.         // 这里由于int_array()本事就是一个无名对象,也就是右值,   
  45.         // 从而自动调用了int_array的转移构造函数,没有不必要的拷贝   
  46.         return  int_array();  // 调用   
  47.   
  48.     int_array result(size); // 调用构造函数   
  49.     va_list  args;  
  50.     va_start(args, size);  
  51.     for  (unsigned  int  i = 0; i < size; ++i)  
  52.     {  
  53.         int  v = va_arg(args,  int );  
  54.         result.set_at(i, v);  
  55.     }  
  56.   
  57.     // 通过move函数将result对象从左值转成右值,   
  58.     // 从而调用int_array的转移构造函数,以减少不必要的拷贝   
  59.     return  int_array(std::move(result));  
  60. }  
  61.   
  62. void  new_example()  
  63. {  
  64.     cout << endl << "enter"  << endl;  
  65.   
  66.     cout << endl << "step1"  << endl;  
  67.   
  68.     // 如果没有RVO,应该调用一次构造函数,两次转移构造函数,没有不必要的拷贝   
  69.     // 由于RVO,实际上只调用了一次构造函数,一次转移构造函数   
  70.     int_array ia1 = new_make_int_array2(10, 20);  
  71.   
  72.     cout << endl << "step2"  << endl;  
  73.   
  74.     // 如果没有RVO,应该调用一次构造函数,一次转移构造函数,一次交换等于操作符,   
  75.     // 由于RVO,实际上只调用了一次构造函数,一次交换等于操作符,没有不必要的拷贝   
  76.     ia1 = make_int_array2(100, 200);  
  77.   
  78.     cout << endl << "step3"  << endl;  
  79.   
  80.     // 如果没有RVO,应该调用一次构造函数,两次转移构造函数,没有不必要的拷贝   
  81.     // 由于RVO,实际上只调用了一次构造函数,一次转移构造函数   
  82.     int_array ia2 = make_int_array(5, 1, 2, 3, 4, 5);  
  83.   
  84.     cout << endl << "step4"  << endl;  
  85.   
  86.     // 调用了一次构造函数,一次转移构造函数,一次交换语义的等于操作符,   
  87.     // 没有不必要的拷贝   
  88.     ia2 = make_int_array(3, 1, 2, 3);  
  89.   
  90.     cout << endl << "exit"  << endl;  
  91. }  
// 拷贝构造函数重载,其实是转移构造函数,接受非常量右值,用于转移内部数据, // 对于常量的右值,由于不能修改内容,还是需要拷贝,正好走传统的拷贝构造函数 int_array(int_array&& src) { cout << " int_array::int_array(int_array&&)" << endl; // 直接转移内部数据,避免了额外的拷贝 m_buffer = src.m_buffer; m_size = src.m_size; src.m_buffer = 0; src.m_size = 0; } // 赋值操作符重载,其实可以看做交换操作符,接受非常量右值,用于交换内部数据 // 对于常量右值,由于无法修改其内部数据,还是需要拷贝,正好走传统的赋值操作符 int_array& operator=(int_array&& rhs) { cout << " int_array& int_array::operator=(int_array&&)" << endl; if (this != &rhs) { // 交换内部数据,这样等下销毁的就是这个对象现在的内部数据了, // 从而避免了不必要的拷贝 swap(m_buffer, rhs.m_buffer); swap(m_size, rhs.m_size); } return *this; } // 生成一个有两个元素的int_arry,采用了转移语义减少拷贝 int_array new_make_int_array2(int v1, int v2) { int_array result(2); // 调用构造函数 result.set_at(0, v1); result.set_at(1, v2); // 通过move函数将result对象从左值转成右值, // 从而调用int_array的转移构造函数,以减少不必要的拷贝 return int_array(std::move(result)); } // 生成一个有size个元素的int_arry,采用了转移语义减少拷贝 int_array new_make_int_array(unsigned int size, ...) { if (size > 10000000) // 如果太大则返回一个空的 // 这里由于int_array()本事就是一个无名对象,也就是右值, // 从而自动调用了int_array的转移构造函数,没有不必要的拷贝 return int_array(); // 调用 int_array result(size); // 调用构造函数 va_list args; va_start(args, size); for (unsigned int i = 0; i < size; ++i) { int v = va_arg(args, int); result.set_at(i, v); } // 通过move函数将result对象从左值转成右值, // 从而调用int_array的转移构造函数,以减少不必要的拷贝 return int_array(std::move(result)); } void new_example() { cout << endl << "enter" << endl; cout << endl << "step1" << endl; // 如果没有RVO,应该调用一次构造函数,两次转移构造函数,没有不必要的拷贝 // 由于RVO,实际上只调用了一次构造函数,一次转移构造函数 int_array ia1 = new_make_int_array2(10, 20); cout << endl << "step2" << endl; // 如果没有RVO,应该调用一次构造函数,一次转移构造函数,一次交换等于操作符, // 由于RVO,实际上只调用了一次构造函数,一次交换等于操作符,没有不必要的拷贝 ia1 = make_int_array2(100, 200); cout << endl << "step3" << endl; // 如果没有RVO,应该调用一次构造函数,两次转移构造函数,没有不必要的拷贝 // 由于RVO,实际上只调用了一次构造函数,一次转移构造函数 int_array ia2 = make_int_array(5, 1, 2, 3, 4, 5); cout << endl << "step4" << endl; // 调用了一次构造函数,一次转移构造函数,一次交换语义的等于操作符, // 没有不必要的拷贝 ia2 = make_int_array(3, 1, 2, 3); cout << endl << "exit" << endl; }
这个示例代码已经完全消除了不必要的拷贝,之前的性能问题得到了彻底地解决。我们发现代码里面用到了std::move()这个函数,接下来我们仔细讲解这个函数。

了解std::move()

很多时候,我们需要把左值引用转换成右值。原因通常有两个:一个原因是我们明 确知道该左值不久后将被销毁,而我们需要转移其中的内部数据到其它对象中,例如上例中new_make_int_array()函数中的用法;另一个原因 是由于右值一旦被具名引用,哪怕是具名的右值引用,它也会变成左值,因此需要将它恢复成右值,这个描述很拗口,但的确就是这样。

标准库中提供了std::move()这个模板函数,用来干这个转换的差事。它接收一个引用,并强制类型转换成右值引用,然后返回。由于函数返回值是不具名的右值引用,因此它还是右值。具体的实现代码如下:

view plain copy to clipboard print ?
  1. template < typename  _Tp>  
  2. inline   typename  std::remove_reference<_Tp>::type&& move(_Tp&& __t)  
  3. {  
  4.     return   static_cast < typename  std::remove_reference<_Tp>::type&&>(__t);  
  5. }  
template<typename _Tp> inline typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

std::move()从名字上看,通常会产生错误的理解,以为移动临时对象 内部数据的操作是由它完成的。看了代码就知道了,根本不是那么回事,它仅仅完成一个语义上的转换,将左值变成右值而已,而且没有任何性能损失。由于 std::move()的语义是转成右值,以便接下来被转移,那么被传入到move函数的参数,在move调用过后,就最好不要再访问了,以免访问到错误 的值。

完美转发(perfect forwarding)

最后我们讲一下完美转发,仔细讲起来会比较复杂。简单来说,在编写函数模板的 过程中,可能需要把模板实参的左右值特性和常量性完美的保持下来,转发给其它的函数。在C++现行标准中,需要为每一个左右值特性和常量性作一个函数重 载,如果函数只有一个形参,需要至少4个重载,如果函数模板有3个参数,则需要4的3次方=64个重载,因此很难做到统一的解决方案。C++0x通过右值 引用,加上引用折叠(reference collapsing),以及左值引用经过函数模板推导还是左值引用的特殊规定,较好的实现了完美转发,就是std::forward()这个模板函数。 其实现代码如下:

view plain copy to clipboard print ?
  1. template < class  _Ty>  
  2. _Ty&& forward(typename  identity<_Ty>::type& _Arg)  
  3. {  
  4.    return  ((_Ty&&)_Arg);  
  5. }  
template<class _Ty> _Ty&& forward(typename identity<_Ty>::type& _Arg) { return ((_Ty&&)_Arg); }

引用折叠(reference collapsing)

补充介绍一下引用折叠,这是C++0x为了实现移动语义(move semantics)和完美转发(perfect forwarding)而增加的规定。简单罗列一下规则,就很清楚了:
A& &等价于A&

A& &&等价于A&

A&& &等价于A&

A&& &&等价于A&&

总结

1. 右值引用的确带来了性能提升的可能,也带来了完美转发。但性能的提升需要额外的编码,用来实现转移和交换行为。

2. 左值和右值的概念,并不是指等号的左边还是右边,而是指该表达式代表的对象是否是持久的,持久的就是左值,反之是右值。我们也可以用是否具名来判断,具名的是左值,反之是右值(非具名左值引用除外)。

3. std::move()函数用于语义上的转移,将左值转成右值,而不是实质上数据的转移,实质性的转移需要每个类单独编码实现。

4. std::forward()函数实现完美转发,可用于模板函数中完美的传递参数。

你可能感兴趣的:(探索C++0x: 3. 右值引用(rvalue reference)(本文前段还行))