0%

右值引用

个人理解,仅供参考!


左值与右值

一个简单的区分方法是,左值是可寻址的,而右值是不可被寻址的。更粗糙地看,一个赋值表达式的左边就是左值,右边就是右值。

左值引用

1
2
int a = 1;
int& ref = a;

这里我们称refa的引用,ref就是一个左值引用。左值引用只可引用左值,尝试引用右值编译器会报错:

alt text

而我们可以通过const来将一个左值引用绑定到一个右值上:

1
const int& ref = 1;

此时我们称ref是一个常量引用,其值不可被更改。

左值引用的局限性

1
2
3
std::vector<my_class> vec;
my_class temp;
vec.push_back(temp);

在这个例子中,我们要在容器vec中添加一个元素,于是创建了一个临时变量temp,再将其添加到容器中。

而实际上,以std::vector::push_back()为例,这个过程中会创建一个temp的副本并添加到数组的末尾,也就是说,会调用my_class的拷贝构造函数。

那这就产生了一个问题,如果my_class的拷贝是深拷贝,那么先创建一个后续大概率不会再使用的临时对象temp,再将其拷贝到容器中,效率太低了。有没有方法直接在容器中创建这个对象?或者有没有办法使得在容器中的新对象,可以直接“窃取”temp的所有资源,而不是再拷贝一份(这也就是所谓的移动语义)?

我们来研究这个问题,以下是std::vector::push_back()的签名:

1
void push_back(const value_type& __x);

可以看到,这个方法的参数类型是常量左值引用,也就意味着传入的参数对象是不可变的。假如我们在这个方法中实现移动语义,新对象可以正确地持有参数对象的资源,但参数对象也仍然持有这些资源。所以我们需要一个新的push_back的重载,它接收一个可变的参数类型,并在新对象获得参数对象的资源后,将参数对象指向资源的指针置空。

右值引用

上面提到的可变的参数类型实际就是右值引用,<type>&&表示<type>类型的右值引用:

1
my_class&& ref = my_class();

有了右值引用之后,我们就可以实现移动语义了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class my_class {
private:
int* data;

public:
//...

//常量左值引用版本的拷贝构造函数
my_class(const my_class& other) : data(new int(*(other.data))) { }

//右值引用版本的拷贝构造函数
my_class(my_class&& other) : data(other.data) {
other.data = nullptr;
}

//...
};

从上面的两版拷贝构造函数中我们可以看到,常量左值引用版本的是拷贝参数对象的资源的值,而右值引用版本则是直接将自身的指针指向参数对象对应的资源,并将参数对象的指针置空。这样,我们就获得了两个实现不同功能的拷贝构造函数,其中,右值引用的版本实现了移动语义。

使用右值引用

有了上面的右值引用版本的拷贝构造函数之后,我们就可以将一个对象的资源“移动”给另一个对象了,这时候我们需要用std::move()来获取原对象的右值引用:

1
2
3
4
5
6
7
//将temp的资源“移动”给obj
my_class temp = my_class();
my_class obj(std::move(temp));

//将obj“移动”到容器中
std::vector<my_class> vec;
vec.push_back(std::move(obj));//实际上,std提供了提供了右值引用版本的push_back重载,所以这里可以传入右值引用

对于std::move()的实现,本文不做深入探讨,我们仅需知道它可以返回一个对象的右值引用即可。