0%

C++11右值引用与移动构造函数

C++11标准新特性

右值引用(Rvalue Reference)是C++11标准引入的特性,它实现了转移语义和精确传递,主要的作用有2个方面:

  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;
  2. 能够更简洁明确地定义泛型函数;

左值引用和右值引用

首先区分什么是左值引用和右值引用,简单的定义:

  • 左值引用:就是命名对象,非临时对象,可以在多条代码中使用的对象;我们通常的变量都是左值;
  • 右值引用:就指非命名对象,临时对象,脱离当前代码语句就无法被引用;比如 函数的返回值;

从左值和右值的定义可以得到,左值引用的对象创建后在代码其他地方会被多次使用,其资源的生命周期较长。而右值引用的对象是临时的,通常用来初始化另一个对象;临时对象创建后很快就会被销毁,其创建-销毁造成了一次资源浪费。既然临时对象将永远不会在其他地方被使用,实际上使用临时对象的资源来初始化另一个对象并不需要一次Copy;更高效的做法将临时对象的资源转移(Move)到另一个命名对象,避免资源的Copy和浪费。

左值和右值的语法符号

C++11中左值的声明符号为&,右值的声明符号为&&

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
void print_value(int& i) {
cout<< "left_value called: "<<i<<endl;
}
void print_value(int&& i) {
cout<< "right_value called: "<<i<<endl;
}

int main() {
int a=0;
print_value(a);
print_value(1);
}

运行结果:

1
2
left_value called: 0
right_value called: 1

print_value函数被重载,分别接受左值和右值。a是命名对象,作为左值处理;而1是临时对象,作为右值处理。

转移语义

右值引用的主要目的是支持转移语义。转移语义可以将资源从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝及销毁,能够大幅度提升C++的性能。临时对象的维护对性能有严重影响。

在C++中,对象的拷贝通过定义拷贝构造函数和拷贝赋值操作符实现。要实现转移语义,需要定义移动构造函数和移动赋值操作符。

转移构造函数和转移赋值操作符

首先直观的来看下各种构造函数的调用情况。假设我们已经实现了类MyClass

1
2
3
4
5
6
MyClass fn();		// 返回MyClass对象的函数
MyClass foo; // 调用默认的构造函数
MyClass bar = foo; // 调用Copy构造函数
MyClass baz = fn(); // 调用Move构造函数
foo = bar; // 调用Copy赋值操作符
baz = MyClass(); // 调用Move赋值操作符

函数fn返回的对象和MyClass()构造的对象都是非命名临时对象,这种情况下没有必要作Copy,将其资源转移到命名对象更高效;所以移动构造函数和移动赋值操作符被调用。

Move构造函数和Move赋值操作符接受自身类对象的右值引用作为参数,其定义如下:

1
2
MyClass (MyClass&&);			//Move Constructor
MyClass& operator= (MyClass&&); //Move Assignment

一个示例程序如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class MyString { 
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
std::cout << "Default Constructor" << std::endl;
}

MyString(const char* p) {
_len = strlen (p);
_init_data(p);
std::cout << "Constructor with: " << p << std::endl;
}

MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}

MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str._data << std::endl;
if (this != &str) {
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}

virtual ~MyString() {
if (_data) free(_data);
}
};

int main() {
MyString a;
a = MyString("Hello");

std::cout << "-----------------" << std::endl;
MyString b = MyString("Test"); //注意与a输出的结果的差异
std::cout << "-----------------" << std::endl;

std::vector<MyString> vec;
vec.push_back(MyString("World"));
}

运行结果:

1
2
3
4
5
6
7
8
Default Constructor
Constructor with: Hello
Move Assignment is called! source: Hello
-----------------
Constructor with: Test
-----------------
Constructor with: World
Move Constructor is called! source: World

上述代码中,MyString("Hello")和MyString("World")都是临时对象,也就是右值;因此C++对右值调用了移动构造函数和移动赋值操作符,避免资源的创建、复制和销毁。

同时,注意到对象a和b两种逻辑等价的写法,导致了完全不同的行为。参考后面的文章《再探C++对象构造》。

移动构造函数和移动赋值操作符的实现需要注意以下几点:

  • 右值参数的符号必须是右值引用符号&&
  • 右值参数不能是const,因为我们需要修改右值
  • 右值参数的资源链接和标记必须修改,否则右值的析构函数会释放资源,转移到新对象的资源也就无效了

在设计实现需要大量分配和释放资源的类是,应该考虑使用移动构造函数和移动赋值操作符来提供效率。

注意点

另外,C++编译器对很多需要调用移动构造函数的情况进行优化,也就是返回值优化,通常发生在当函数返回值被用于初始化一个对象时。这种情况下,移动构造函数不会被调用,会变为直接调用构造函数。

Compilers already optimize many cases that formally require a move-construction call in what is known as Return Value Optimization. Most notably, when the value returned by a function is used to initialize an object. In these cases, the move constructor may actually never get called.

还需要注意,右值引用很少用于移动构造函数之外的其他地方,非必要的使用会增加程序debug的难度。

std::move

已经知道,所有的命名对象都是左值引用,而编译器只对右值引用才会调用转移语义。如果能够确定一个命名对象将不再被使用,就可以对它调用转移语义,也就是把一个左值当作右值引用来使用。为了实现这种用法,C++标准库提供了函数std::move,这个函数将左值引用转为右值引用。

本质上,std::move并不想其名字表示的那样,它并没有做任何移动资源的操作,它只是一个语法糖,可以用lvalue_to_rvalue去理解它。

std::move可以用来提高swap函数的性能,一般来说,swap函数的通用定义如下:

1
2
3
4
5
template <class T> swap(T& a, T& b) {
T tmp(a); //copy a to tmp
a = b; //copy b to a
b = tmp; //copy tmp to b
}

其实这里有3次不必要的拷贝操作,可以通过std::move转换为右值来避免:

1
2
3
4
5
template <class T> swap(T& a, T& b) {
T tmp(std::move(a)); //move a to tmp
a = std::move(b); //move b to a
b = std::move(tmp); //move tmp to b
}

小结

右值引用和转移语义是C++11标准一个重要特性,在优化C++代码时,应该考虑使用右值和转移语义来避免不必要的拷贝操作。

参考:

右值引用与转移语义

C++ Special members