C++11新特性:右值引用和转移构造函数

问题背景

 

[cpp] view plaincopy

 

  1. #include <iostream>  
  2.    
  3. using namespace std;  
  4.    
  5. vector<int> doubleValues (const vector<int>& v)  
  6. {  
  7.     vector<int> new_values( v.size() );  
  8.     for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )  
  9.     {  
  10.         new_values.push_back( 2 * *itr );  
  11.     }  
  12.     return new_values;  
  13. }  
  14.    
  15. int main()  
  16. {  
  17.     vector<int> v;  
  18.     for ( int i = 0; i < 100; i++ )  
  19.     {  
  20.         v.push_back( i );  
  21.     }  
  22.     v = doubleValues( v );  
  23. }  

先来分析一下上述代码的运行过程。

 

 

[cpp] view plaincopy

 

  1. vector<int> v;  
  2. for ( int i = 0; i < 100; i++ )  
  3. {  
  4.     v.push_back( i );  
  5. }  

以上5行语句在栈上新建了一个vector的实例,并在里面放了100个数。

 

[cpp] view plaincopy

 

  1. v = doubleValues( v )  

这条语句调用函数doubleValues,函数的参数类型的const reference,常量引用,那么在实参形参结合的时候并不会将v复制一份,而是直接传递引用。所以在函数体内部使用的v就是刚才创建的那个vector的实例。

 

但是

 

[cpp] view plaincopy

 

  1. vector<int> new_values( v.size() );  

这条语句新建了一个vector的实例new_values,并且复制了v的所有内容。但这是合理的,因为我们这是要将一个vector中所有的值翻倍,所以我们不应该改变原有的vector的内容。

[cpp] view plaincopy

 

  1. v = doubleValues( v );  

 

函数执行完之后,new_values中放了翻倍之后的数值,作为函数的返回值返回。但是注意,这个时候doubleValue(v)的调用已经结束。开始执行 = 的语义。

赋值的过程实际上是将返回的vector<int>复制一份放入新的内存空间,然后改变v的地址,让v指向这篇内存空间。总的来说,我们刚才新建的那个vector又被复制了一遍。

但我们其实希望v能直接得到函数中复制好的那个vector。在C++11之前,我们只能通过传递指针来实现这个目的。但是指针用多了非常不爽。我们希望有更简单的方法。这就是我们为什么要引入右值引用和转移构造函数的原因。

 

左值和右值

在说明左值的定义之前,我们可以先看几个左值的例子。

[cpp] view plaincopy

 

  1. int a;  
  2. a = 1; // here, a is an lvalue  

上述的a就是一个左值。

临时变量可以做左值。同样函数的返回值也可以做左值。

[cpp] view plaincopy

 

  1. int x;  
  2. int& getRef ()   
  3. {  
  4.         return x;  
  5. }  
  6.    
  7. getRef() = 4;  

以上就是函数返回值做左值的例子。

 

其实左值就是指一个拥有地址的表达式。换句话说,左值指向的是一个稳定的内存空间(即可以是在堆上由用户管理的内存空间,也可以是在栈上,离开了一个block就被销毁的内存空间)。上面第二个例子,getRef返回的就是一个全局变量(建立在堆上),所以可以当做左值使用。

 

与此相反,右值指向的不是一个稳定的内存空间,而是一个临时的空间。比如说下面的例子:

[cpp] view plaincopy

 

  1. int x;  
  2. int getVal ()  
  3. {  
  4.     return x;  
  5. }  
  6. getVal();  

这里getVal()得到的就是临时的一个值,没法对它进行赋值。
下面的语句就是错的。

[cpp] view plaincopy

 

  1. getVal() = 1;//compilation error  

所以右值只能够用来给其他的左值赋值。

 

右值引用

在C++11中,你可以使用const的左值引用来绑定一个右值,比如说:

[cpp] view plaincopy

 

  1. const int& val = getVal();//right  
  2. int& val = getVal();//error  

因为左值引用并不是左值,并没有建立一片稳定的内存空间,所以如果不是const的话你就可以对它的内容进行修改,而右值又不能进行赋值,所以就会出错。因此只能用const的左值引用来绑定一个右值。

 

在C++11中,我们可以显示地使用“右值引用”来绑定一个右值,语法是"&&"。因为指定了是右值引用,所以无论是否const都是正确的。

[cpp] view plaincopy

 

  1. const string&& name = getName(); // ok  
  2. string&& name = getName(); // also ok   

有了这个功能,我们就可以对原来的左值引用的函数进行重载,重载的函数参数使用右值引用。比如下面这个例子:

[cpp] view plaincopy

 

  1. printReference (const String& str)  
  2. {  
  3.         cout << str;  
  4. }  
  5.    
  6. printReference (String&& str)  
  7. {  
  8.         cout << str;  
  9. }  

可以这么调用它。

[cpp] view plaincopy

 

  1. string me( "alex" );  
  2. printReference(  me ); // 调用第一函数,参数为左值常量引用  
  3.    
  4. printReference( getName() ); 调用第二个函数,参数为右值引用。  

好了,现在我们知道C++11可以进行显示的右值引用了。但是我们如果用它来解决一开始那个复制的问题呢?

这就要引入与此相关的另一个新特性,转移构造函数和转移赋值运算符

 

转移构造函数和转移赋值运算符

假设我们定义了一个ArrayWrapper的类,这个类对数组进行了封装。

[cpp] view plaincopy

 

  1. class ArrayWrapper  
  2. {  
  3.     public:  
  4.         ArrayWrapper (int n)  
  5.             : _p_vals( new int[ n ] )  
  6.             , _size( n )  
  7.         {}  
  8.         // copy constructor  
  9.         ArrayWrapper (const ArrayWrapper& other)  
  10.             : _p_vals( new int[ other._size  ] )  
  11.             , _size( other._size )  
  12.         {  
  13.             for ( int i = 0; i < _size; ++i )  
  14.             {  
  15.                 _p_vals[ i ] = other._p_vals[ i ];  
  16.             }  
  17.         }  
  18.         ~ArrayWrapper ()  
  19.         {  
  20.             delete [] _p_vals;  
  21.         }  
  22.     private:  
  23.     int *_p_vals;  
  24.     int _size;  
  25. };  

我们可以看到,这个类的拷贝构造函数显示新建了一片内存空间,然后又对传进来的左值引用进行了复制。

如果传进来的实际参数是一个右值(马上就销毁),我们自然希望能够继续使用这个右值的空间,这样可以节省申请空间和复制的时间。

我们可以使用转移构造函数实现这个功能:

[cpp] view plaincopy

 

  1. class ArrayWrapper  
  2. {  
  3. public:  
  4.     // default constructor produces a moderately sized array  
  5.     ArrayWrapper ()  
  6.         : _p_vals( new int[ 64 ] )  
  7.         , _size( 64 )  
  8.     {}  
  9.    
  10.     ArrayWrapper (int n)  
  11.         : _p_vals( new int[ n ] )  
  12.         , _size( n )  
  13.     {}  
  14.    
  15.     // move constructor  
  16.     ArrayWrapper (ArrayWrapper&& other)  
  17.         : _p_vals( other._p_vals  )  
  18.         , _size( other._size )  
  19.     {  
  20.         other._p_vals = NULL;  
  21.     }  
  22.    
  23.     // copy constructor  
  24.     ArrayWrapper (const ArrayWrapper& other)  
  25.         : _p_vals( new int[ other._size  ] )  
  26.         , _size( other._size )  
  27.     {  
  28.         for ( int i = 0; i < _size; ++i )  
  29.         {  
  30.             _p_vals[ i ] = other._p_vals[ i ];  
  31.         }  
  32.     }  
  33.     ~ArrayWrapper ()  
  34.     {  
  35.         delete [] _p_vals;  
  36.     }  
  37.    
  38. private:  
  39.     int *_p_vals;  
  40.     int _size;  
  41. };  

第一个构造函数就是转移构造函数。它先将other的域复制给自己。尤其是将_p_vals的指针赋值给自己的指针,这个过程相当于int的复制,所以非常快。然后将other里面_p_vals指针置成NULL。这样做有什么用呢?

我们看到,这个类的析构函数是这样的:

[cpp] view plaincopy

 

  1. ~ArrayWrapper ()  
  2.     {  
  3.         delete [] _p_vals;  
  4.     }  

它会delete掉_p_vals的内存空间。但是如果调用析构函数的时候_p_vals指向的是NULL,那么就不会delte任何内存空间。

所以假设我们这样使用ArrayWrapper的转移构造函数:

[cpp] view plaincopy

 

  1. ArrayWrapper *aw = new ArrayWrapper((new ArrayWrapper(5)));  

其中

[cpp] view plaincopy

 

  1. (new ArrayWrapper(5)  

获得的实例就是一个右值,我们不妨称为r,当整条语句执行结束的时候就会被销毁,执行析构函数。

所以如果转移构造函数中没有

[cpp] view plaincopy

 

  1. other._p_vals = NULL;  

的话,虽然aw已经获得了r的_p_vals的内存空间,但是之后r就被销毁了,那么r._p_vals的那片内存也被释放了,aw中的_p_vals指向的就是一个不合法的内存空间。所以我们就要防止这片空间被销毁。

 

右值引用也是左值

这种说法可能有点绕,来看一个例子:

 

我们可以定义MetaData类来抽象ArrayWrapper中的数据:

[cpp] view plaincopy

 

  1. class MetaData  
  2. {  
  3. public:  
  4.     MetaData (int size, const std::string& name)  
  5.         : _name( name )  
  6.         , _size( size )  
  7.     {}  
  8.    
  9.     // copy constructor  
  10.     MetaData (const MetaData& other)  
  11.         : _name( other._name )  
  12.         , _size( other._size )  
  13.     {}  
  14.    
  15.     // move constructor  
  16.     MetaData (MetaData&& other)  
  17.         : _name( other._name )  
  18.         , _size( other._size )  
  19.     {}  
  20.    
  21.     std::string getName () const { return _name; }  
  22.     int getSize () const { return _size; }  
  23.     private:  
  24.     std::string _name;  
  25.     int _size;  
  26. };  

那么ArrayWrapper类现在就变成这个样子

[cpp] view plaincopy

 

  1. class ArrayWrapper  
  2. {  
  3. public:  
  4.     // default constructor produces a moderately sized array  
  5.     ArrayWrapper ()  
  6.         : _p_vals( new int[ 64 ] )  
  7.         , _metadata( 64, "ArrayWrapper" )  
  8.     {}  
  9.    
  10.     ArrayWrapper (int n)  
  11.         : _p_vals( new int[ n ] )  
  12.         , _metadata( n, "ArrayWrapper" )  
  13.     {}  
  14.    
  15.     // move constructor  
  16.     ArrayWrapper (ArrayWrapper&& other)  
  17.         : _p_vals( other._p_vals  )  
  18.         , _metadata( other._metadata )  
  19.     {  
  20.         other._p_vals = NULL;  
  21.     }  
  22.    
  23.     // copy constructor  
  24.     ArrayWrapper (const ArrayWrapper& other)  
  25.         : _p_vals( new int[ other._metadata.getSize() ] )  
  26.         , _metadata( other._metadata )  
  27.     {  
  28.         for ( int i = 0; i < _metadata.getSize(); ++i )  
  29.         {  
  30.             _p_vals[ i ] = other._p_vals[ i ];  
  31.         }  
  32.     }  
  33.     ~ArrayWrapper ()  
  34.     {  
  35.         delete [] _p_vals;  
  36.     }  
  37. private:  
  38.     int *_p_vals;  
  39.     MetaData _metadata;  
  40. };  

同样,我们使用了转移构造函数来避免代码的复制。但是这里的转移构造函数对吗?

问题出在下面这条语句

[cpp] view plaincopy

 

  1. _metadata( other._metadata )  

我们希望的是other._metadata是一个右值,然后就会调用MetaData类的转移构造函数来避免数据的复制。但是很可惜,右值引用是左值。

在前面已经说过,左值占用了内存上一片稳定的空间。而右值是一个临时的数据,离开了某条语句就会被销毁。other是一个右值引用,在ArrayWrapper类的转移构造函数的整个作用域中都可以稳定地存在,所以确实占用了内存上的稳定空间,所以是一个左值,因为上述语句调用的并非转移构造函数。所以C++标准库提供了如下函数来解决这个问题:

[cpp] view plaincopy

 

  1. std::move  

这条语句可以将左值转换为右值

 

[cpp] view plaincopy

 

  1. // 转移构造函数  
  2.   ArrayWrapper (ArrayWrapper&& other)  
  3.       : _p_vals( other._p_vals  )  
  4.       , _metadata( std::move( other._metadata ) )  
  5.   {  
  6.       other._p_vals = NULL;  
  7.   }  

这样就可以避免_metadata域的复制了。

 

函数返回右值引用

 

我们可以在函数中显示地返回一个右值引用

 

[cpp] view plaincopy

 

  1. int x;  
  2.    
  3. int getInt ()  
  4. {  
  5.     return x;  
  6. }  
  7.    
  8. int && getRvalueInt ()  
  9. {  
  10.     // notice that it's fine to move a primitive type--remember, std::move is just a cast  
  11.     return std::move( x );  
  12. }  

感谢Alex Allain提供的部分代码例子。http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

本文是在他的文章的基础之上写出来的。

时间: 2024-11-16 02:50:10

C++11新特性:右值引用和转移构造函数的相关文章

浅析C++11中的右值引用、转移语义和完美转发_C 语言

1. 左值与右值:     C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值.     可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值.     从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)

漫谈C++11利器之右值引用(move语义&amp;Perfect Forwarding)

该文章来自阿里巴巴技术协会(ATA) 作者:空溟  C++11(2011)标准推出已经很长时间了,最接地气的特性就要属"右值引用"了(Rvalue Reference),它实现了move语义和完美转发(Perfect Forwarding),一开始觉得不好理解,所以一直想对其做一个总结.网上也有很多牛人已经做了细致的分析,但基本都是讲原理的多,本文就从Rvalue Reference引入动机入手,举例说明右值引用的使用场景,从而引出move语义和完美转发. 1. 右值引用动机: 从一个

C++的新标准:右值引用和转移语义

右值引用 (Rhttp://www.aliyun.com/zixun/aggregation/9541.html">value Referene) 是 C++++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding).它的主要目的有两个方面: 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率. 能够更简洁明确地定义 泛型函数. 左值与右值的定义 C++

《深入理解C++11:C++ 11新特性解析与应用》——3.3 右值引用:移动语义和完美转发

3.3 右值引用:移动语义和完美转发 类别:类作者 3.3.1 指针成员与拷贝构造 对C++程序员来说,编写C++程序有一条必须注意的规则,就是在类中包含了一个指针成员的话,那么就要特别小心拷贝构造函数的编写,因为一不小心,就会出现内存泄露.我们来看看代码清单3-16中的例子. 在代码清单3-16中,我们定义了一个HasPtrMem的类.这个类包含一个指针成员,该成员在构造时接受一个new操作分配堆内存返回的指针,而在析构的时候则会被delete操作用于释放之前分配的堆内存.在main函数中,我

c++ 11 移动语义、std::move 左值、右值、将亡值、纯右值、右值引用

为什么要用移动语义 先看看下面的代码 // rvalue_reference.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> class HugeMem { public: HugeMem(int size) : sz(size) { pIntData = new int[sz]; } HugeMem(const HugeMem & h) : sz(h.sz) { pIntData =

C++标准之(ravalue reference) 右值引用介绍_C 语言

1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

左值、右值与右值引用

在C语言中,我们常常会提起左值(lvalue).右值(rvalue)这样的称呼.而在编译程序时,编译器有时也会在报出的错误信息中会包含 左值.右值的说法.不过左值.右值通常不是通过一个严谨的定义而为人所知的,大多数时候左右值的定义与其判别方法是一体的.一个最为典型的判别方法就是, 在赋值表达式中,出现在等号左边的就是"左值",而在等号右边的,则称为"右值".比如: a = b + c; 在这个赋值表达式中,a就是一个左值,而b + c则是一个右值.这种识别左值.右值

VC10中的C++0x特性 Part 2 (1):右值引用

本文为 Part 2 的第一页 今天我要讲的是 rvalue references (右值引用),它能实现两件不同的事情: move 语意和完美转发.刚开始会觉得它们难以理解,因为需要区分 lvalues 和 rvalues ,而只有极少数 C++98/03 程序员对此非常熟悉.这篇文章会很长,因为我打算极其详尽地解释 rvalue references 的运作机制. 不用害怕,使用 ravlue references 是很容易的,比听起来要容易得多.要在你的代码中实现 move semanti

c++11-一个关于C++11右值引用的问题

问题描述 一个关于C++11右值引用的问题 代码如下: class Foo { public: Foo sorted() const &;//---------------成员函数1 Foo sorted() const &&;//-------------成员函数2 }; Foo Foo::sorted() const & { cout << "sorted() const & " << endl; return Foo