移动语义

Left value, Right value and Move Semantics

右值引用

虽然c++11对value catagories进行了新划分,但最初c++沿用了c对于一个表达式的分类,只有左值(lvalue)和右值(rvalue),能获取内存地址的就是左值,不能的就是右值。对于左值的引用就是左值引用,对于右值的引用就是右值引用。

1
2
3
int val = 6;
int& lref = a; // left value reference
int&& rref = 6; // right value reference

严格来讲,左值引用不能引用右值,右值引用不能引用左值,但是添加了const限定的左值引用可以引用右值:

1
const int& lref = 6;

拷贝构造

假设我们现在有一个简单的对象,对象里有一个整型数组和一个记录数组大小的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Array {
private:
size_t size_;
int* data_;
public:
explicit Array(const size_t size): size_(size), data_(new int[size_]) {}

~Array() {
std::cout << "deconstructing" << std::endl;
if (data != nullptr) {
delete[] data_;
size_ = 0;
}
}

// copy assginment constructor
Array(const Array& other): size_(other.size_), data_(new int[size_]) {
std::cout << "deep copying" << std::endl;
std::copy(other.data_, other.data_ + size_, data_);
}
};

在第二个拷贝函数中,我们传入了另一个Array对象并试图把该Array对象中的数据拷贝进入当前构造的对象当中去。很显然,这个传入的Array是一个左值,引用便是左值引用,同时构造函数中需要深拷贝。但假若我需要拷贝一个很大很复杂的对象,深拷贝的代价就会非常大。显然,我们需要一个更好的方法,比如是否可以直接把传入的Array对象里的数据直接拿过来,而非拷过来?

移动构造

这个时候右值引用就派上用场了。因为右值没有地址,可以直接复制给左值,那么只要传入一个右值在进行赋值就可以避免深拷贝了。

1
2
3
4
5
6
// move assignment constructor
Array(Array&& other): size_(other.size_), data_(other.data_) {
std::cout << "move constructor" << std::endl;
arr.data_ = nullptr;
arr.size_ = 0;
}

这时我们只要在构造中传入一个右值,就可以进行内存所有权的转移,从而避免进行深拷贝,进而减少性能消耗。同时,在移动构造中一定要释放源对象的指针,防止析构重复释放内存。

赋值运算符

赋值运算符也可以用移动语义来优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// copy assignment operator.
Array& operator=(const Array& other)
{
std::cout << "Copy operator" << std::endl;

if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}

// move assignment operator.
Array& operator=(Array&& other)
{
std::cout << "Move operator" << std::endl;

if (this != &other) {
// free exsiting memory.
delete[] data_;

size_ = other.size_;
data_ = other.data_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}

std::move()

还有一个问题没有解决,就是如何传入一个右值。c++在std中提供了一个move函数,这个函数虽然叫move,但它的功能非常简单,就是单纯的把一个左值变为右值。说白了,就是对static_cast<T&&>进行的封装。

1
2
// For an existing Array object A
Array B(std::move(A));

此处A是一个左值,我们希望调用移动构造器,便使用move将A变为右值并传入。传入后A的内存数据被直接转移给了B,A中的内容被置空,等待析构。

此外,std::move()一般不用于基本类型,基本类型没有必要进行内存移动,移动速度不一定比拷贝快。

Reference

Understanding lvalues and rvalues in C and C++

What is move semantics?

Move Constructors and Move Assignment Operators (C++)

Author

s.x.

Posted on

2021-10-16

Updated on

2022-04-10

Licensed under

Comments