右值引用 (Rvalue Referene) 是 C++ 新标准中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:
(1)消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
(2)能够更简洁明确地定义泛型函数。
虽然本篇的题目是右值引用,但是会延伸到一些其他的相关概念,包含:move语义、移动构造和移动赋值函数、完美转发、深拷贝和浅拷贝等相关概念。
1.相关概念
(1)左值与右值
要想了解右值引用,就要先区分左值和右值。在C++( 包括 C) 中,对于左值和右值最通俗的解释就是,可以放在等号左边的就是左值,只能放在等号右边的就是右值。(另外一种解释是:左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效)但是以上2种解释在C++语言的编译过程中存在一些特殊的情况:
上面的代码中,一般认为:s1 + s2 是右值,临时对象也是右值,但是放在等号左侧编译是可以通过的。所以与其记住左值和右值的定义,不如记住常见的左值和右值的情况。请看下列示例 :
在int i = 1 + 2;中 i 是左值,1 + 2 是右值;在A a = getA(); getA()的返回值是是临时对象,是右值,a值左值。
当然网上有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。可以适当判断左值和右值。
(2)左值引用和右值引用
左值引用用&符号引用 左值(但不能引用右值)
右值引用用&&符号引用 右值(也可以指向移动左值)
(3)move语义
在之前的分析中,确定了左值和右值的范围,总体的感觉是,右值的范围更小,左值的范围更大,左值应该是包含右值的,但是从编译器的角度,例如在函数传参时,Func(a); a 是一个左值,Func(A()); A()是一个右值,如何将一个左值当做右值来处理,这就需要使用C++11新提供的std::move()函数了,该函数的作用就是将左值当成右值使用。当左值和当右值的区别会在下面说明。
(4)深拷贝和浅拷贝
由于C++语言支持指针类型的变量,所以当一个类中有指针的成员变量时,就会涉及到深拷贝和浅拷贝的问题,浅拷贝就是将指针复制给另一个对象,也就是说两个对象的指针指向了同一块内存(这很有可能出问题);深拷贝就是不仅复制指针,而且还将指针所指向的内容复制一份,各自指针管理各自的内存空间。
(5)拷贝构造(赋值)函数和移动构造(赋值)函数
在C++之前的版本中,只有拷贝构造(赋值)函数,对于有指针成员变量的类中,需要自定义深拷贝的逻辑,在C++11新标准中引入了移动构造(赋值)函数,移动构造的本质就是抢占原来对象的属性给自己,并且是原来对象失效。下面以MyString的拷贝构造函数和移动构造函数为例,看看各自都做了什么。
首先是拷贝构造函数,对于指针成员变量,需要将指针所指向的内存复制到新对象中
如下图所示,在内存中,每个对象的指针指向了不同的内存空间,两个对象互不影响。
在移动构造函数中,无论是否是指针,都是浅拷贝,但是对于指针成员变量,切断原来对象中指针与内存的指向关系,所以一旦采用了移动构造,原来对象一般就不能再使用了。
所以综合来看,移动构造需要发生在右值上,因为,右值一般都是临时对象,只能使用一次,无法再次使用的。
(6)完美转发
为什么会需要完美转发呢?我们建立了一些一个场景。
在forward()函数中调用process()函数,调用forward时,传入的参数2是右值,但是当传递到process()函数时,变成左值,最终的结果如下:
在函数传递的过程中导致表达式有左值变成左值,这就是不完美的转发,想要完美转发,C++11新标准中提供了一个新的函数std::forwar<T>(),可以保证在调用时不会发生改变。
将forward()函数修改成下面的代码:
最终的结果就可以保证右值的特性。
2. 实例代码
说了一堆概念,移动构造(move语义)有什么优势呢?最大的用处就是将一些不再使用的对象重复利用,减少了拷贝的过程,从而提高性能。
下面以在vector容器中插入MyString对象为例,看一下移动构造对性能的影响,以及需要注意的一些问题。
当采用拷贝构造的方式在vector中放入10000000个MyString对象,所用的时间是3500ms。
当使用移动构造时,所用的时间是1828ms
可以看出,移动构造对于vector容器的性能提升还是很高的。
关于移动构造(赋值)函数中noexcept的说明。
首先需要知道的是,vector在扩容的过程中其实是存在大量的拷贝动作的,如果是原来的拷贝构造,将原来的数据复制一份,又将原来的数据内存空间释放,这一过程对性能的损失是很大的。这一过程如果采用移动构造来进行刚好可以利用原来的数据又可以提高性能,那么如何让编译器采用移动构造函数呢?这就要依靠noexcept声明了,告诉编译器移动构造(赋值)函数是不会跑出异常的,可以放心使用。
向vector中存放10个MyString对象,如果有noexcept声明,结果是如下
如果没有noexcept声明,结果就是在vector提升容量时采用拷贝构造的方式。
3.总结
对于C++11的右值引用等特性,不同的编译器支持的程度可能不同。从上述的示例中可能看出,合理的使用右值引用,移动构造等特性可以显著提升程序性能。右值引用所包含的知识点很多,如有需要后续再做补充。
参考
侯捷老师的c++11新特性视频
https://www.jianshu.com/p/d19fc8447eaa
https://blog.csdn.net/qq_41949110/article/details/107033147
https://www.cnblogs.com/dj0325/p/6854403.html