问题背景

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

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

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

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

  1. v = doubleValues( v )

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

但是

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

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

  1. v = doubleValues( v );

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

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

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

左值和右值

在说明左值的定义之前,我们可以先看几个左值的例子。
  1. int a;
  2. a = 1; // here, a is an lvalue

上述的a就是一个左值。

临时变量可以做左值。同样函数的返回值也可以做左值。
  1. int x;
  2. int& getRef ()
  3. {
  4. return x;
  5. }
  6. getRef() = 4;

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

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

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

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

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

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

 

右值引用

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

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

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

 
在C++11中,我们可以显示地使用“右值引用”来绑定一个右值,语法是"&&"。因为指定了是右值引用,所以无论是否const都是正确的。
  1. const string&& name = getName(); // ok
  2. string&& name = getName(); // also ok

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

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

可以这么调用它。

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

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

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

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

假设我们定义了一个ArrayWrapper的类,这个类对数组进行了封装。
  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. };

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

如果传进来的实际参数是一个右值(马上就销毁),我们自然希望能够继续使用这个右值的空间,这样可以节省申请空间和复制的时间。
我们可以使用转移构造函数实现这个功能:
  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. ArrayWrapper (int n)
  10. : _p_vals( new int[ n ] )
  11. , _size( n )
  12. {}
  13. // move constructor
  14. ArrayWrapper (ArrayWrapper&& other)
  15. : _p_vals( other._p_vals  )
  16. , _size( other._size )
  17. {
  18. other._p_vals = NULL;
  19. }
  20. // copy constructor
  21. ArrayWrapper (const ArrayWrapper& other)
  22. : _p_vals( new int[ other._size  ] )
  23. , _size( other._size )
  24. {
  25. for ( int i = 0; i < _size; ++i )
  26. {
  27. _p_vals[ i ] = other._p_vals[ i ];
  28. }
  29. }
  30. ~ArrayWrapper ()
  31. {
  32. delete [] _p_vals;
  33. }
  34. private:
  35. int *_p_vals;
  36. int _size;
  37. };

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

我们看到,这个类的析构函数是这样的:
  1. ~ArrayWrapper ()
  2. {
  3. delete [] _p_vals;
  4. }

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

所以假设我们这样使用ArrayWrapper的转移构造函数:
  1. ArrayWrapper *aw = new ArrayWrapper((new ArrayWrapper(5)));

其中

  1. (new ArrayWrapper(5)

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

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

  1. other._p_vals = NULL;

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

 

右值引用也是左值

这种说法可能有点绕,来看一个例子:
 
我们可以定义MetaData类来抽象ArrayWrapper中的数据:
  1. class MetaData
  2. {
  3. public:
  4. MetaData (int size, const std::string& name)
  5. : _name( name )
  6. , _size( size )
  7. {}
  8. // copy constructor
  9. MetaData (const MetaData& other)
  10. : _name( other._name )
  11. , _size( other._size )
  12. {}
  13. // move constructor
  14. MetaData (MetaData&& other)
  15. : _name( other._name )
  16. , _size( other._size )
  17. {}
  18. std::string getName () const { return _name; }
  19. int getSize () const { return _size; }
  20. private:
  21. std::string _name;
  22. int _size;
  23. };

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

  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. ArrayWrapper (int n)
  10. : _p_vals( new int[ n ] )
  11. , _metadata( n, "ArrayWrapper" )
  12. {}
  13. // move constructor
  14. ArrayWrapper (ArrayWrapper&& other)
  15. : _p_vals( other._p_vals  )
  16. , _metadata( other._metadata )
  17. {
  18. other._p_vals = NULL;
  19. }
  20. // copy constructor
  21. ArrayWrapper (const ArrayWrapper& other)
  22. : _p_vals( new int[ other._metadata.getSize() ] )
  23. , _metadata( other._metadata )
  24. {
  25. for ( int i = 0; i < _metadata.getSize(); ++i )
  26. {
  27. _p_vals[ i ] = other._p_vals[ i ];
  28. }
  29. }
  30. ~ArrayWrapper ()
  31. {
  32. delete [] _p_vals;
  33. }
  34. private:
  35. int *_p_vals;
  36. MetaData _metadata;
  37. };

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

问题出在下面这条语句
  1. _metadata( other._metadata )

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

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

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

 
  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域的复制了。

 

函数返回右值引用

 
我们可以在函数中显示地返回一个右值引用
 
  1. int x;
  2. int getInt ()
  3. {
  4. return x;
  5. }
  6. int && getRvalueInt ()
  7. {
  8. // notice that it's fine to move a primitive type--remember, std::move is just a cast
  9. return std::move( x );
  10. }
本文是在他的文章的基础之上写出来的。

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

  1. 【转】C++11 标准新特性: 右值引用与转移语义

    VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...

  2. C++11 标准新特性: 右值引用与转移语义

    文章出处:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/ 新特性的目的 右值引用 (Rvalue Referene) ...

  3. C++ 新特性-右值引用

    作为最重要的一项语言特性,右值引用(rvalue references)被引入到 C++0x中.我们可以通过操作符“&&”来声明一个右值引用,原先在C++中使用“&”操作符声明 ...

  4. c++右值引用和转移构造函数

    int &&i = ; //i绑定到了右值1 int b = ; cout << i << endl; //输出1 i = b; cout << i ...

  5. [转载] C++11中的右值引用

    C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...

  6. C++11特性-右值引用

    什么是左值,什么是右值 常见的误区有 = 左边的是左值,右边的是右值. 左值:具有存储性质的对象,即lvalue对象,是指要实际占用内存空间.有内存地址的那些实体对象,例如:变量(variables) ...

  7. C++ 11 中的右值引用

    C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include &l ...

  8. C++11中的右值引用

    原文出处:http://kuring.me/post/cpp11_right_reference May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移 ...

  9. C++11 右值引用和转移语义

    新特性的目的 右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和 ...

随机推荐

  1. Visual Studio Gallery

    Web Essentials :对CSS.JavaScript和HTML都提供了很多快捷的功能支持.http://vswebessentials.com/features/general Web Co ...

  2. 检测SqlServer服务器CPU是否瓶颈

    初次写博文,分享个人心得,欢迎大虾小虾来拍砖. 系统自带的性能监视器 在开始命令框中输入perfmon按enter键即可打开性能监视器 可以通过监视 % Processor Time 的值察看cpu是 ...

  3. [C#]Winform下回车或Tab键自动切换下一个控件焦点

    满足用户体验,在数据录入时,能在输入完一个信息后通过回车或Tab键自动的切换到下一个控件(字段). 在界面控件设计时,默认可以通过设置控件的TabIndex来实现.但在布局调整时或者是对输入的内容有选 ...

  4. 文件流操作(FileStream,StreamReader,StreamWriter)

    大文件拷贝: /// <summary> /// 大文件拷贝 /// </summary> /// <param name="sSource"> ...

  5. Page 的生命周期学习小结(翻译兼笔记)

    初始化(Initialization) 页面被请求时,第一个被执行的总是下面接着执行的是 接着是 然后是 恢复和加载(Restore and Load) 接下来的 ViewState 被取回后,接着  ...

  6. easyui扩展-日期范围选择.

    参考: http://www.5imvc.com/Rep https://github.com/dangrossman/bootstrap-daterangepicker * 特性: * (1)基本功 ...

  7. 打开shpfile,mdb,sde工作空间

    打开shapefile工作空间: ESRI.ArcGIS.Geodatabase.IWorkspaceFactory wsf = new ESRI.ArcGIS.DataSourcesFile.Sha ...

  8. (UVALive 7261)Xiongnu's Land 二分

    题目链接:https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_ ...

  9. PerformSelector may cause a leak because its selector is unknown 解决方法

    我的技术博客经常被流氓网站恶意爬取转载.请移步原文:http://www.cnblogs.com/hamhog/p/3801030.html,享受整齐的排版.有效的链接.正确的代码缩进.更好的阅读体验 ...

  10. struts2使用struts2-bootstrap-plugin插件

    1.下载插件 http://code.google.com/p/struts2-bootstrap/ 2.添加maven依赖 <dependency> <groupId>com ...