左值右值
什么是左值和右值
左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。
比如:
1 |
|
其中a
和b
是左值,5和A()
是右值。
左值引用和右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。
左值引用
能指向左值,不能指向右值的就是左值引用
1 |
|
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但可以通过const左值引用指向右值。编译器将创建一个临时对象来持有这个右值,这个临时变量是可以进行取地址操作,并且在 var
的生命周期内,该临时对象都是有效的。
1 |
|
const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const &
作为函数参数的原因之一,如std::vector
的push_back
:
1 |
|
如果没有const
,vec.push_back(5)
这样的代码就无法编译通过了。
右值引用
右值引用的标志是&&
,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:
1 |
|
可以通过std::move
函数使右值引用指向左值。
在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。
std::move
是一个非常有迷惑性的函数,不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)
。 所以,单纯的std::move(xxx)不会有性能提升
总结
- 被声明出来的左、右值引用都是左值。
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
应用场景
为什么要有右值引用呢,在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数
、拷贝构造函数
、赋值运算符重载
、析构函数
等。
1 |
|
该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数
,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,如:
1 |
|
这么做有2个问题:
- 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
- 无法实现!
temp_array
是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;
这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){...}
,这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);
这种调用方式就没法用了。
右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以右值引用为参数的移动构造函数
和移动赋值重载函数
,或者其他函数,最常见的如std::vector的push_back
和emplace_back
。参数为左值引用意味着拷贝,为右值引用意味着移动。
1 |
|
移动构造函数
上面我们说过,为了解决避免深拷贝的麻烦,我们引入了移动构造函数,也就是移动语义,将一个对象中的资源移动到另一个对象(资源控制权的转移)。
拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:
- 拷贝构造函数的参数是 const左值引用,接收左值或右值;
- 移动构造函数的参数是右值引用,接收右值或被 move 的左值。
比如书vector
的push_back
1 |
|
当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。
总的来说,如果这两个函数都有在类内定义的话,在构造对象时:
- 若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
- 若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。
比如下面的代码:
1 |
|
移动赋值
转移参数右值的资源来赋给自己。
1 |
|
拷贝赋值函数和移动赋值函数都是赋值运算符重载函数的重载函数,所不同的是:
- 拷贝赋值函数的参数是 const左值引用,接收左值或右值;
- 移动赋值函数的参数是右值引用,接收右值或被 move 的左值。
总的来说,如果这两个函数都有在类内定义的话,在进行对象的赋值时:
- 若是左值做参数,那么就会调用拷贝赋值,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝赋值就会做一次深拷贝)。
- 若是右值做参数,那么就会调用移动赋值,而调用移动赋值就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动赋值就会少做一次深拷贝)
比如下面的代码:
1 |
|
unique pointer
理解了左值和右值,我觉得可以顺带讲一下C++里面的unique_ptr
。
还有些STL类是move-only
的,就比如unique_ptr
,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):
所以需要
1 |
|
需要申明移动构造函数和移动赋值函数:
1 |
|
这样我们就不可以写出下面的代码
1 |
|
只能下面这样写(调用移动赋值函数或者移动构造函数):
1 |
|