参考https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
转移语义的目的
右值引用(RValue Reference)是C++11引入的新特性,它是实现了转移语义(move sementics)和精确传递(Perfect Forwarding),主要目的是:
- 消除两个对象相互交互时不必要的对象拷贝,节省运算存储资源,提高运行效率
- 能够更简洁明确地定义泛型函数。
左值与右值的定义
C++(C)的表达式和变量要么是左值,要么是右值
- 左值的定义就是非临时对象。在下边的多条代码中可以被使用的对象。所有的变量都是左值。能够改变就是特征。
- 右值是临时的对象,他们只在当前的语句中有效。
1 | //例子 |
既然右值可以被修改,那么就可以实现右值引用。右值引用能够方便地解决工程实际问题。
C++11中 左值声明符号为&,右值为&&
转移语义
右值引用是用来支持转移语义的。转移语义可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高c++应用程序的性能。临时对象的维护(创建和销毁)对性能有严重的影响
转移语义与拷贝语义是相对的。类比当我们将一个文件拷贝到另一个地址时,会比剪切慢很多。
通过转移语义,临时对象的资源能够转移到其他的对象里。也就是将右值转移成了一个左值,而没有使用拷贝构造函数或者拷贝赋值运算符。
C++11之前的拷贝构造函数和拷贝赋值运算符
1 | class MyString { |
MyString(“Hello”)和MyString(“World”)都使用了构造函数来生成临时对象,也就是右值。在右值语句结束之后,临时对象就会被销毁。这个过程中,如果能直接将临时对象申请的资源转换到左值对象的话,就可以节省资源,并且节省资源申请和释放的时间
转移构造函数
1 | MyString(MyString&& str) { |
它和拷贝构造函数有点类似,但有几点需要注意:
- 参数符号必须是右值&&
- 右值参数不可以是常量!因为我们要修改右值(右值不是常量哦,常量是不能够修改的变量)
- 右值参数的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
转移赋值操作符
1 | MyString& operator=(MyString&& str) { |
同样需要注意以上三点。
使用了转移语义之后,编译器会区分左值和右值。运行结果如下
1 | Move Assignment is called! source: Hello |
在设计和实现类的时候,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率
标准库函数 std::move
既然编译器只对优质引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用。如果已知一个左值对象不想再被使用二线对他调用转移构造函数和转移赋值运算符,也就是将一个左值引用当做一个右值引用来使用。std::move()就提供了这个方法。
1 | void ProcessValue(int& i) { |
运行结果是:
1 | LValue processed: 0 |
如果要实现swap的话。std::move会极大提升函数性能
1 | //一般swap函数 |
注意std::move只是将参数转换为了右值,比如std::move(a)并没有将a丢掉,只是返回了右值。
1 | //简单的move实现 |
真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。
1 | T (T&& rhs); |
精确传递Perfect Forwarding
Perfect Forwaring适用于将一组参数原封不动的传递给另一个函数。”原封不动“不仅仅是参数的值不变,在C++中除了参数值之外,还有以下两组属性:
- 左值、右值
- const, non-const
精确传递就是在参数传递过程中,这些属性和参数值都不能改变。这在泛型函数中,这样的需求非常普遍。
举个例子:
1 | template <typename T> |
forward_value为每一个参数必须重载两种类型,T&和const T&.对于编写函数重载的次数是和参数类型成正比的关系。
使用右值引用:
1 | template <typename T> |
这里只需要定义一次,接受一个右值引用的参数,就能将左右的参数类型原封不动的传递给目标函数。可以简洁的调用不同类型的参数
1 | int a = 0; |
推导规则
C++11中定义T&&
的推导规则是:
右值实参为右值引用,左值实参仍然为左值引用 一句话就是参数的属性不变。