C++11学习笔记之4——右值引用(rvalue reference)和移动语义(Move Semantics)

本文所谈到的两个标准在VS2012中已经可以实现

1、右值引用(rvalue reference)

左值是可以获取到地址的一个量,具有变量名便可以作为左值来引用(例如int count = 10;),通常出现在赋值符号左边.
而右值引用是C++11引入的一个新概念,用来引用那些会自动销毁的临时对象,主要目的是对这些临时对象提供移动复制构造函数和移动operator=(稍后解释),但是如果深入研究或许还会有更多新的用途被发掘。
在C++中,下面的代码是不合法的,显而易见:

1
int& i = 2;  // Invalid,无法为引用类型 i 获取到有意义的实际地址,所以这个变量无法被声明

而如果在C++11中,可以通过&&(注意不是“与”操作)符号来声明一个右值引用,它会去调用默认的移动构造函数(或者移动operator=).

1
int&& i = 2 + 5; // OK

这样写一般没有多大意义,更多的意义是在右值引用被作为参数传递时带来的性能提升.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Proc(int& lvalue)// a reference of lvalue,必须要左值
{
++lvalue;
cout<< "increment the lvalue`data: " << lvalue <<endl;
}

void Proc(int&& rvalue)// a reference of rvalue
{
++rvalue;
cout<< "increment the rvalue`data: " << rvalue <<endl;
}

int a = 9 , b = 10 ,c = 2;
Proc(a); // call Proc(int& lvalue)

// 如果未定义右值引用参数类型的版本,那么下面将会出现编译错误,原因在下面会讲解
Proc(b+c); // call Proc(int&& rvalue)
//自动将b+c表达式生成的临时对象引用为参数,这个引用过程牵涉到移动语义

// Output
10
13

对于编译器报错“无法将int型转化为int&”我们可以这么理解:熟悉汇编的同学应该知道 Proc(b+c)的值传递过程,是由b和c的值在相加后产生的中间值赋值给寄存器作为参数传递给函数,而寄存器中的变量是临时变量,不同于堆栈上的变量(可以由引用地址确定一个唯一的变量值)。因此由于无法将临时的整型变量转化为有效的地址引用,编译器将无法通过。

另外无法同时定义Proc(int vlaue)和Proc(int& value),因为这两个函数在遇到一个普通int参数时会产生歧义,二者都可以被调用。同样的无法同时定义Proc(int vlaue)和Proc(int&& value),原因是由于在遇到一个临时int变量时,二者也都是可以被调用的,同样有二义性。因此同时定义Proc(int& value)和Proc(int&& value)成为新的选择。下面的遇到的示例也是和这个相同的原因。

所以接下来的讨论都是以引用作为参数,如果参数类型是值传递 int value,那么就不存在这些问题了(未定义Proc(int&& rvalue),Proc(b+c)也可以正常运作,它将很自然地调用值传递版本),右值引用就是为了减少参数副本开销而用的.

C++11提供了std::move()函数将左值强制转换为右值,下面会演示它的用法.

2、移动语义(Move Semantics)

移动语义实现了移动构造函数(move copy constructor)和移动赋值预算符(move assignment operator).

移动语义将上一概念中的右值引用引出的临时对象转化为可以被引用的左值(在参数场合下,即转为实参),这个动作实际上提供了程序员操作临时对象内存的能力(the possibility to modify the temporary object or memory),并且允许我们自由地选择移动构造和移动赋值的操作

1
2
3
MyClass(MyClass&& src); // move copy constructor
MyClass& operator=(MyClass&& rhs); // move assignment operator
//参数都是none-const,因为拷贝构造(或赋值操作)的来源是个即将销毁的临时对象

考虑下面的例子:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class MyClass
{
public:
MyClass(double vlaue/*,int* ptr*/);

// Normal copy constructor
MyClass(const MyClass& src);
// Normal assignment operator
MyClass& operator=(const MyClass& rhs);

// Move copy constructor
MyClass(MyClass&& src); // remove const attribute
// Move assignment operator
MyClass& operator=(MyClass&& rhs); // remove const attribute

double m_data;
//int* m_pBuffer; //如果存在拥有某块内存的情况
};

MyClass::MyClass(double value /*,int* ptr*/)
:m_data(value)//,m_pBuffer(ptr)
{
}

MyClass::MyClass(const MyClass& src)
{
m_data = src.m_data;

/*
do some Deep copy 深拷贝 memcpy(m_pBuffer,src.m_pBuffer......);
*/
cout << "Normal copy constructor" <<endl;
}

MyClass& MyClass::operator=(const MyClass& rhs)
{
if(this == &rhs)
{
return *this;
}

m_data = rhs.m_data;
/*
do some Deep copy 深拷贝
*/

cout << "Normal assignment operator" <<endl;
return *this;
}

MyClass::MyClass(MyClass&& src)
{
m_data = src.m_data;

// shallow copy 浅拷贝,仅仅是拷贝指针
// 临时对象的内存被传递出来
//m_pBuffer = src.m_pBuffer;

src.m_data = 0;
// 临时对象之后会被销毁,必须在转移内存所有权后重置为空指针
//src.m_pBuffer = nullptr;
cout << "Move copy constructor" <<endl;
}

MyClass& MyClass::operator=(MyClass&& rhs)
{
if(this == &rhs)
{
return *this;
}

m_data = rhs.m_data;
// shallow copy 浅拷贝,仅仅是拷贝指针
// 临时对象的内存被传递出来
//m_pBuffer = src.m_pBuffer;

// 临时对象之后会被销毁,必须在转移内存所有权后重置为空指针
//src.m_pBuffer = nullptr;
cout<< "Move assignment operator" <<endl;
return *this;
}

MyClass CreateObject(double value)
{
return MyClass(value);
}

/* Can not both define Func(MyClass a) and Func(MyClass&& a)
void Func(MyClass a)
{
cout<< "Called Func(MyClass a)" << endl;
}
*/

// Also can not both define Func(MyClass a) and Func(MyClass& a)
void Func(MyClass& a)
{
cout<< "Called Func(MyClass& a)" << endl;
}

// Both define Func(MyClass& a) and Func(MyClass&& a) is OK
void Func(MyClass&& a)
{
cout<< "Called Func(MyClass&& a)" << endl;
}

int main(int argc, char* argv[])
{
// call MyClass(double value)
MyClass a(3.0);
// call Func(MyClass& a)
Func(a);

// These 3 lines call Func(MyClass&& a) all, or they will call Func(MyClass a) if it`s defined
Func(3.0);
Func(std::move(2.0));
Func(CreateObject(1.0));

cout<< "Before move assignment, a.m_data = "<<a.m_data << endl;
// 已存在的对象被改写,调用赋值预算
// call operator=(MyClass&& rhs),not operator=(const MyClass& rhs)
a = CreateObject(4.0);

cout<< "After move assignment, a.m_data = "<<a.m_data << endl;
// call MyClass::MyClass(MyClass&& src)
MyClass d(std::move(a));
cout<< "After move copy constructor,"<< endl;
cout<< "a.m_data = "<< a.m_data << endl;
cout<< "d.m_data = "<<d.m_data << endl;

return 0;
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
Called Func(MyClass& a)
Called Func(MyClass&& a)
Called Func(MyClass&& a)
Called Func(MyClass&& a)
Before move assignment, a.m_data = 3
Move assignment operator
After move assignment, a.m_data = 4
Move copy constructor
After move copy constructor,
a.m_data = 0
d.m_data = 4

现在我们加上析构函数:

1
2
3
4
MyClass::~MyClass()
{
cout<<"Deconstructor called"<<endl;
}

通过vector来查看各个函数被调用情况,vector在增加元素时会重新分配内存空间,这时候vector内部会重新开辟空间,并复制构造出新对象,然后销毁原内存中的旧对象。代码如下:

1
2
3
vector<MyClass&gt;	vec;
vec.push_back(CreateObject(9.0));
vec.push_back(std::move(10.0));

在未定义移动拷贝构造函数MyClass(MyClass&& src)时,调用普通拷贝构造函数MyClass(const MyClass& src)执行深拷贝,输出如下:

1
2
3
4
5
6
7
8
Normal copy constructor
Deconstructor called
Normal copy constructor
Deconstructor called
Normal copy constructor
Deconstructor called
Deconstructor called
Deconstructor called

在同时定义移动拷贝构造函数MyClass(MyClass&& src)和普通拷贝构造函数MyClass(const MyClass& src)时,输出如下:

1
2
3
4
5
6
7
8
Move copy constructor
Deconstructor called
Move copy constructor
Deconstructor called
Move copy constructor
Deconstructor called
Deconstructor called
Deconstructor called

可见在临时对象发生右值引用时会查询是否存在移动构造函数(或移动赋值运算)的定义,如果存在,将会选择后者执行更快的临时对象数据获取,否则执行默认的深拷贝.

可以以C++11之2——类的构造中相同的方式= default(或=delete)来开启或禁用两个函数.