移动语义

lvalue and rvalue

左值,即赋值符号左边的值,当然也可以出现在赋值符号右边,可以通过去地址运算符获取其地址是左值的根本特点;

右值,即赋值符号右边的值,只有数值的概念,在内存中没有一块固定的空间用于存放,如表达式中的数值、表达式运算的结果等等,如果熟悉汇编语言,可以理解为右值暂时存放在寄存器中。c++11为了引入右值引用,将右值进一步划分为纯右值将亡值

  • 纯右值,即纯粹的右值,包括没有标识符、不可取地址的表达式和字面量(注意与字符串字面量区分,双引号括起来的字符串是存放在内存的可读区的)。
  • 将亡值,临时对象,暂时存放在内存中,一般马上会被销毁,如函数返回的一个对象等等,这部分内存是可以被移动的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a;
a = 2; // true. a is lvalue and 2 is rvalue
a = 3; // true. a is lvalue and 3 is rvalue
2 + 3 = 9; // error. 2+ 3 is rvalue

int b = 3;
int *pb = &b; // true. b is lvalue,可以取地址操作
&b = 12; // error.取地址操作返回的是b的地址,是一个右值

int & foo(); // foo返回一个左值引用
foo() = 12; // true. 可以通过一个左值引用给左值赋值
int * pf = &foo(); // true. 对左值引用取地址等同于对其引用的对象取地址

int bar(); // bar返回一个int类型的值,为右值
int c = bar(); // 将bar返回的值拷贝给c
int * pb = &bar(); // error. bar返回的值是临时存在的

左值引用与右值引用

左值引用,即引用对象是左值的引用;右值引用,即引用对象是右值的引用。以往我们直接称左值引用为引用,并使用&表示,为了区分左值引用,使用&&表示右值引用。使用规则如下:

  • 左值引用,使用T&,只能绑定左值
  • 右值引用,使用T&&,只能绑定右值
  • 常量左值引用,const T&,可以绑定左值或右值,但是不能对其修改。
1
2
3
4
5
6
7
int i = 10;
int & ri = i;
int && rri1 = i; // error. i is a lvalue
int && rri2 = 10; // true. i is a rvalue
int && rri3 = 8 + 10 * i; // true. 8 + 10 * i is a rvalue
const int & cri1 = i; // const T&绑定到左值
const int & cri2 = 2 + 3; // const T&绑定到右值

Note:右值引用本身也是一个左值。如下fun为重载函数,一个接受左值参数,另一个接受右值参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void fun(int &a) {
std::cout << "in fun(int &)" << std::endl;
}

void fun(int &&a) {
std::cout << "in fun(int &&)" << std::endl;
}

int main()
{
int a = 1;
int &&b = 1;

fun(a);
fun(b);
fun(std::move(a)); // 将左值转换为右值引用类型
fun(1+3);

return 0;
}

编译运行结果如下,可见fun(b)调用的是左值版本。

1
2
3
4
5
6
> g++ b.cc -g -o a
> ./a
in fun(int &)
in fun(int &)
in fun(int &&)
in fun(int &&)

std::move

左值具有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。绑定到临时对象的右值引用,其引用的对象将要被销毁且无其他用户,因此我们可以接管其占有的资源。

虽然不能将一个右值引用绑定到左值上,但是可以通过move显示地将一个左值转换为对应的右值引用。上一节最后,使用了move作用于a,使得其从一个左值转换为右值引用类型。需要注意的是,可以销毁一个移后源对象,也可以赋予它新值,但是不能使用其值,因为其值已经和之前的状态不同了(比如被设置成初始状态)。

Why move

程序运行过程中,可能会产生许多的临时对象,这些临时对象拷贝到我们指定的左值,然后被销毁,如果一个对象占有大量的内存,则需要重复的产生和销毁,对程序的性能造成影响。

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
class Obj
{
public:
Obj()
{
cout << "Obj default-constructor\n";
}
Obj(const Obj &)
{
cout << "Obj copy-constructor\n";
}
~Obj()
{
cout << "Obj destructor\n";
}
};

int main()
{
vector<Obj> vo;
vo.reserve(4);
vo.push_back(Obj());

return 0;
}

编译运行,输出如下:

1
2
3
4
5
> g++ b.cc -O0 -o a && ./a
Obj default-constructor
Obj copy-constructor
Obj destructor
Obj destructor

可见,首先调用了默认构造函数,生成一个临时对象,然后将该临时对象拷贝一份存入vector,再销毁该临时对象。如果临时对象本身占有大量的资源,这些资源的拷贝和销毁会成为降低程序的性能。

由于临时对象本身没有其他用户且马上需要被销毁,我们可以直接接管其资源,既可以减轻销毁临时对象的工作,也无需拷贝一份重复的资源。或者有时候我们不需要使用一个左值了,此时move就派上了用场。类似拷贝构造函数和拷贝赋值运算符,c++11允许我们为类定义移动构造函数和移动赋值运算符,它们从给定的对象中“窃取”资源而非拷贝。

move-constructor and move-assign

重新设计一个类名为Array,其动态管理一个int数组,因此它需要管理自身占有的资源,首先思考这个类需要什么功能,即需要对外提供的接口:

  • 初始化:根据给定的size和默认值初始化一个Array
  • 拷贝构造和拷贝赋值:支持深拷贝
  • 添加元素:类似vector的push_back
  • 返回size:返回array对象的元素个数
  • 返回容量:返回array对象的capacity
  • 销毁:析构函数需要负责销毁自身管理的int数组
1
2
3
4
class Array
{

};

编写main函数测试该类:

1
2
3
4
int main()
{

}

生成时机

上一节讨论了移动语义在实际编程中的用法和优点,类似拷贝操作,编译器有时会为我们默认合成移动操作,这些默认的移动操作有时候就能满足我们的编程需求。但是有时候编译器不会提供默认合成的移动操作,而将其定义为删除的函数,此时如果我们在程序中显示要求使用移动操作,则编译器会默认使用拷贝操作替代,从而程序的行为不符合我们的预期。本节讨论如下内容:

  • 默认移动操作的行为:移动每一个数据成员,其中内置类型的成员编译器可以移动,类类型的成员需要有对应的移动操作。
  • 何时编译器会为类合成移动操作:类如果定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数,则编译器不会为它合成移动操作。如果我们没有定义移动操作,且编译器也没有为我们合成时,即使我们指定使用移动操作,编译器会默默使用拷贝操作替代。此时,我们可以自定义移动操作,或者显式要求编译器为我们合成默认操作(=default)。
  • 何时移动操作被定义为删除,即显式要求一个移动操作而编译器无法为其生成,编译器会将移动操作定义为删除的函数。此时如果我们需要移动操作,必须自定义,而不能显示要求编译器默认为我们合成。
  • 显示定义移动操作会对默认合成的拷贝操作有什么影响:定义了移动操作(移动拷贝函数和移动赋值运算符之一),则拷贝操作默认被定义为删除。必须自定义或者显式要求编译器生成。

Note:需要区分编译器为我们合成移动操作时,是不会还是不能

Prepare

设计三个类,分别为Dad、Son和Daughter,其中Dad类拥有一个Son类型的数据成员和一个Daughter类型的数据成员,可以理解为父亲类有一对儿女。

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
class Daughter
{
public:
Daughter()=default;
Daughter(const Daughter &)
{
cout << "Daughter copy-constructor\n";
}
Daughter(Daughter &&)
{
cout << "Daughter move-constructor\n";
}
Daughter & operator=(const Daughter &)
{
cout << "Daughter copy-assign\n";
return *this;
}
Daughter & operator=(Daughter &&)
{
cout << "Daughter move-assign\n";
return *this;
}
};

class Son
{
public:
Son()=default;
Son(const Son &)
{
cout << "Son copy-constructor\n";
}
Son(Son &&)
{
cout << "Son move-constructor\n";
}
Son & operator=(const Son &)
{
cout << "Son copy-assign\n";
return *this;
}
Son & operator=(Son &&)
{
cout << "Son move-assign\n";
return *this;
}
};

class Dad
{
public:
Dad()=default;
Dad(const Dad &)=default;
Dad(Dad &&)=default;
Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
~Dad()=default;
public:
Son son;
Daughter daughter;
};

我们通过如下测试程序,根据打印的信息,探索移动操作的行为。c++默认的拷贝操作和移动操作,是拷贝或移动每个非static成员,因此,如果拷贝或移动了Dad对象,则其数据成员也会被拷贝或移动;如果拷贝赋值或者移动赋值了Obj对象,则其mem成员也会被拷贝赋值或者移动赋值。此时会打印处提示信息,我们就知道了Dad对象是被拷贝还是移动了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
o2 = o1;
o2 = std::move(o1);

return 0;
}

编译运行,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son move-assign
Daughter move-assign

与预期结果相同,此时我们显式要求了编译器为我们提供移动操作和拷贝操作,并且编译器通过匹配调用了我们需要的操作。

Verify 1

验证:一个类定义了拷贝构造函数、拷贝赋值运算符或析构函数,则不会合成移动操作,编译器使用拷贝操作替代

注释掉拷贝操作、移动操作和析构函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

再次编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son move-assign
Daughter move-assign

此时编译器的输出和最初的情况相同,即默认为我们生成了拷贝操作和移动操作,并在合适的地方调用了。

然后,显式要求编译器合成默认的拷贝构造函数、拷贝赋值运算符和析构函数中的任一几个(为节省篇幅仅给出显式合成析构函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
~Dad()=default;
public:
Son son;
Daughter daughter;
};

编译运行,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son copy-constructor
Daughter copy-constructor
Son copy-constructor
Daughter copy-constructor
Son copy-constructor
Daughter copy-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son copy-assign
Daughter copy-assign

此时,编译器使用拷贝操作替代了移动操作,即使我们没有显式要求合成拷贝操作。此时,其对象是通过拷贝操作来实现“移动”的。

Verify 2

验证:定义了移动操作的类必须定义拷贝操作,否则拷贝操作被定义为删除的

显式要求合成移动构造函数,其余保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

编译,出现报错:

1
2
3
4
5
6
7
8
9
10
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:101:10: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
101 | o2 = o1;
| ^~
b.cc:74:7: note: ‘Dad& Dad::operator=(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~
b.cc:102:22: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
102 | o2 = std::move(o1);

此时main函数中的如下两行,被编译器认定使用了删除的拷贝赋值运算符:

1
2
o2 = o1;
o2 = std::move(o1);

可见,仅仅定义了移动构造函数,移动复制运算符会被拷贝赋值运算符替代。尝试定义移动赋值而注释掉移动构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

尝试编译仍然出错,因为编译器也未默认合成移动构造函数,而是使用拷贝构造函数替代之,但是声明了移动赋值运算符,拷贝操作均被定义为删除。

因此我们必须同时定义两种移动操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

同时,我们修改main函数,仅仅测试拷贝赋值是否成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
// vo.push_back(dad); // 左值,拷贝
// vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
o2 = o1;
o2 = std::move(o1);

return 0;
}

编译,出现错误(一般IDE也会提示报错而无需编译):

1
2
3
4
5
6
7
8
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:101:10: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
101 | o2 = o1;
| ^~
b.cc:74:7: note: ‘Dad& Dad::operator=(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~

提示我们使用了删除的拷贝赋值,因为Dad类声明了移动构造函数或移动赋值运算符。

再次修改main函数测试拷贝构造函数,首先验证移动构造可以成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
// vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son move-assign
Daughter move-assign

再次修改main函数测试拷贝构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
// vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
// vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
// o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
/usr/include/c++/11/ext/new_allocator.h:162:11: error: use of deleted function ‘Dad::Dad(const Dad&)’
162 | { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
b.cc:74:7: note: ‘Dad::Dad(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~

同上,提示我们因为Dad类声明了一个移动构造函数或移动赋值运算符,拷贝构造函数被定义为删除。

Verify 3

验证:类成员的移动操作不可见或者被定义为删除时,类的移动操作也被定义为删除

Verify 4

验证:类似拷贝赋值,如果存在类成员是被const修饰的常量或者一个引用,则其移动赋值被定义为删除

为Dad类添加一个int类型常量成员i:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dad
{
public:
Dad() {}
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
const int i{};
Son son;
Daughter daughter;
};

修改main函数,测试构造函数部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
// o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========

修改main函数测试移动赋值部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
o2 = std::move(o1);

return 0;
}

编译出错,提示我们移动赋值操作被定义为删除的:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:105:22: error: use of deleted function ‘Dad& Dad::operator=(Dad&&)’
105 | o2 = std::move(o1);
| ^
b.cc:76:7: note: ‘Dad& Dad::operator=(Dad&&)’ is implicitly deleted because the default definition would be ill-formed:
76 | class Dad
| ^~~
b.cc:76:7: error: non-static const member ‘const int Dad::i’, cannot use default assignment operator

同样的步骤我们可以为Dad类添加一个引用类型成员,并逐步修改main函数测试,编译出错的原因是相同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i;

class Dad
{
public:
Dad() : ci(i) {}
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
int & ci;
Son son;
Daughter daughter;
};

编译器提示的错误信息如下:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:105:22: error: use of deleted function ‘Dad& Dad::operator=(Dad&&)’
105 | o2 = std::move(o1);
| ^
b.cc:76:7: note: ‘Dad& Dad::operator=(Dad&&)’ is implicitly deleted because the default definition would be ill-formed:
76 | class Dad
| ^~~
b.cc:76:7: error: non-static reference member ‘int& Dad::ci’, cannot use default assignment operator

可见,当存在类成员为引用类型或者是const,此时不允许赋值,这是显而易见的。