移动构造函数和移动赋值函数
本节阅读量:
前面我们讨论了std::auto_ptr,说明了移动语义的需求,并分析了将为复制语义设计的函数(拷贝构造函数和拷贝赋值运算符)重新定义为移动语义时会出现的一些缺点。
在本课中,我们将更深入地了解C++11如何通过移动构造函数和移动赋值函数解决这些问题。
回顾拷贝构造函数和拷贝赋值函数
首先,让我们花点时间回顾一下复制语义。
拷贝构造函数用于通过复制同一类的对象来初始化对象。拷贝赋值函数用于将一个类对象复制到另一个已经存在的类对象。默认情况下,如果没有显式提供拷贝构造函数和拷贝赋值函数,C++会提供默认版本。这些由编译器提供的函数会执行浅拷贝,这可能导致分配动态内存的类出现问题。因此,处理动态内存的类应该重写这些函数来执行深拷贝。
回到本章第一课中的Auto_ptr智能指针类示例。让我们看看一个实现了拷贝构造函数和拷贝赋值函数(执行深拷贝)的版本,以及使用它们的示例程序:
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
|
#include <iostream>
template<typename T>
class Auto_ptr3
{
T* m_ptr {};
public:
Auto_ptr3(T* ptr = nullptr)
: m_ptr { ptr }
{
}
~Auto_ptr3()
{
delete m_ptr;
}
// 拷贝构造函数
// 从 a.m_ptr 到 m_ptr 的深拷贝
Auto_ptr3(const Auto_ptr3& a)
{
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// 拷贝赋值
// 从 a.m_ptr 到 m_ptr 的深拷贝
Auto_ptr3& operator=(const Auto_ptr3& a)
{
// 自我赋值检查
if (&a == this)
return *this;
// 释放已经持有的资源
delete m_ptr;
// 拷贝资源
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
Auto_ptr3<Resource> generateResource()
{
Auto_ptr3<Resource> res{new Resource};
return res; // 这里会触发拷贝构造函数
}
int main()
{
Auto_ptr3<Resource> mainres;
mainres = generateResource(); // 这里会触发拷贝赋值函数
return 0;
}
|
在这个程序中,我们使用一个名为generateResource()的函数创建一个由智能指针封装的资源,然后将其传回main()。随后main()将它赋值给已有的Auto_ptr3对象。
运行此程序时,它将打印:
1
2
3
4
5
6
|
Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed
|
(注意:如果编译器在函数generateResource()中省略了返回值构造,则只能看到4行输出)
对于这样一个简单程序来说,这产生了大量资源创建和销毁!这是怎么回事?
让我们仔细看看。此程序中发生了6个关键步骤(每个打印消息一个):
- 在generateResource()中,局部变量res被动态创建,打印出第一个“Resource acquired”
- res按值返回给main()函数。因为res是局部变量,所以不能按地址或按引用返回,否则会导致悬空引用。因此,res被拷贝给一个临时对象。因为这是一次深拷贝,一个新的Resource对象被分配出来并调用拷贝构造函数,所以打印出第二个“Resource acquired”
- res超出作用域,第一个创建的Resource对象被销毁,打印出第一个“Resource destroyed”
- 临时对象被赋值给mainres,发生拷贝赋值。因为这是一次深拷贝,所以一个新的Resource对象被分配出来,打印出另一个“Resource acquired”
- 赋值表达式结束,临时对象超出作用域并被销毁,打印出第二个“Resource destroyed”
- 在main()函数末尾,mainres超出作用域并被销毁,打印出最后一个“Resource destroyed”
简而言之,因为调用了一次拷贝构造函数将res复制到临时对象,又调用了一次赋值将临时对象复制到mainres,所以我们总共分配并销毁了3个独立对象。
效率低下,但至少不会崩溃!
然而,有了移动语义,我们可以做得更好。
移动构造函数和移动赋值
C++11定义了两个服务于移动语义的新函数:移动构造函数和移动赋值运算符。拷贝构造函数和拷贝赋值的目标是将一个对象复制到另一个对象,而移动构造函数和移动赋值的目标是将资源所有权从一个对象移动到另一个对象(这通常比制作副本便宜得多)。
定义移动构造函数和移动赋值函数的方式类似于拷贝版本。然而,这些函数的拷贝版本采用常量左值引用参数(它几乎可以绑定到任何东西),而移动版本使用非常量右值引用参数(只能绑定到右值)。
下面是与上面相同的Auto_ptr3类,但添加了移动构造函数和移动赋值运算符。为了便于比较,我们保留了执行深拷贝的拷贝构造函数和拷贝赋值函数。
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
|
#include <iostream>
template<typename T>
class Auto_ptr4
{
T* m_ptr {};
public:
Auto_ptr4(T* ptr = nullptr)
: m_ptr { ptr }
{
}
~Auto_ptr4()
{
delete m_ptr;
}
// 拷贝构造函数
// 从 a.m_ptr 到 m_ptr 的深拷贝
Auto_ptr4(const Auto_ptr4& a)
{
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// 移动构造函数
// a.m_ptr 的所有权转移至 m_ptr
Auto_ptr4(Auto_ptr4&& a) noexcept
: m_ptr(a.m_ptr)
{
a.m_ptr = nullptr; // 这一行后面会详细解释
}
// 拷贝赋值
// 从 a.m_ptr 到 m_ptr 的深拷贝
Auto_ptr4& operator=(const Auto_ptr4& a)
{
// 自我赋值检查
if (&a == this)
return *this;
// 释放已经持有的资源
delete m_ptr;
// 拷贝资源
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
// 移动赋值函数
// a.m_ptr 的所有权转移至 m_ptr
Auto_ptr4& operator=(Auto_ptr4&& a) noexcept
{
// 自我赋值检查
if (&a == this)
return *this;
// 释放已经持有的资源
delete m_ptr;
// a.m_ptr 的所有权转移至 m_ptr
m_ptr = a.m_ptr;
a.m_ptr = nullptr; // 这一行后面会详细解释
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
Auto_ptr4<Resource> generateResource()
{
Auto_ptr4<Resource> res{new Resource};
return res; // 这一行会触发移动构造函数
}
int main()
{
Auto_ptr4<Resource> mainres;
mainres = generateResource(); // 这一行会触发移动赋值函数
return 0;
}
|
移动构造函数和移动赋值运算符很简单。我们不是将源对象(a)深拷贝到隐式对象中,而是直接移动(窃取)源对象的资源。这涉及将源指针浅拷贝到隐式对象中,然后将源指针设置为null。
运行时,此程序打印:
1
2
|
Resource acquired
Resource destroyed
|
这好多了!
main()中的流程与之前完全相同。然而,该程序不是调用拷贝构造函数和拷贝赋值函数,而是调用移动构造函数和移动赋值运算符。再深入一点:
- 在generateResource(),局部变量res被动态创建,打印出第一个“Resource acquired”
- res按值返回给main()函数,res被移动构造到一个临时对象中,动态创建的Resource所有权被转移到了临时对象。下面小节会详细分析为什么会发生这样的行为
- res超出作用域,但因为它不再管理任何指针,所以无事发生
- 临时对象被移动赋值到mainres,动态创建的Resource所有权被转移到mainres
- 赋值语句结束,临时对象被销毁,但因为临时对象不再管理任何指针,所以无事发生
- main()函数末尾,mainres超出作用域被销毁,打印出最后一个“Resource destroyed”
因此,我们不是复制资源两次,而是转移它两次。这更高效,因为Resource只构造和销毁一次,而不是三次。
相关内容
移动构造函数和移动赋值应标记为noexcept。这告诉编译器这些函数不会引发异常。
我们在后面介绍noexcept。
何时调用移动构造函数和移动赋值函数?
当定义了这些函数,并且构造或赋值的参数是右值时,就会调用移动构造函数和移动赋值函数。最典型的情况是,值是字面值或临时值。
否则会使用拷贝构造函数和拷贝赋值(例如参数是左值时,或者参数是右值但未定义移动构造函数或移动赋值函数时)。
隐式移动构造函数和移动赋值运算符
如果满足以下所有条件,编译器将创建隐式移动构造函数和移动赋值运算符:
- 没有用户声明的拷贝构造函数或拷贝赋值函数。
- 没有用户声明的移动构造函数或移动赋值运算符。
- 没有用户声明的析构函数。
这两个默认函数都会执行成员级移动,规则如下:
- 如果成员变量有移动构造或移动赋值函数,则会调用对应函数
- 否则,进行拷贝
这意味着,如果成员变量是指针,则默认进行浅拷贝。
移动语义背后的关键点
现在您有了足够的上下文来理解移动语义背后的关键点。
如果我们构造一个对象或进行赋值,而参数是左值,那么唯一合理的做法就是复制这个左值。我们不能假设更改左值是安全的,因为它可能会在后续程序中再次使用。如果有一个表达式“a=b”(其中b是左值),就不应该期望b以任何方式改变。
然而,如果我们构造一个对象或进行赋值,而参数是右值,那么我们知道右值只是某种临时对象。我们可以简单地将其资源(代价很低)转移到正在构造或赋值的对象,而不是复制它(这可能很昂贵)。这样做是安全的,因为临时对象无论如何都会在表达式末尾被销毁,所以我们知道它不会再次被使用!
C++11通过右值引用,让我们能够在参数为右值和左值时提供不同的行为,从而根据对象的使用方式做出更聪明、更高效的决策。
移动函数应始终使两个对象都处于有效状态
在上面的示例中,移动构造函数和移动赋值函数都将a.m_ptr设置为nullptr。这似乎是多余的:毕竟,如果a是一个临时右值,并且无论如何都要被销毁,为什么还要费心进行“清理”呢?
答案很简单:当a超出作用域时,会调用a的析构函数,并删除a.m_ptr。如果此时a.m_ptr仍然指向与m_ptr相同的对象,则m_ptr会变成悬空指针。当包含m_ptr的对象最终被使用(或销毁)时,将产生未定义行为。
在实现移动语义时,重要的是确保资源被移出的对象仍处于有效状态,以便它能被正确销毁(而不会导致未定义行为)。
函数按值返回可以触发移动而不是拷贝
在上面的Auto_ptr4示例的generateResource()函数中,当变量res按值返回时,它会被移动而不是复制,即使res是左值。C++规范有一个特殊规则:按值从函数返回的对象即使是左值,也可以被移动。这是有意义的,因为res无论如何都会在函数末尾被销毁!与其制作昂贵且不必要的副本,不如直接窃取它的资源。
尽管编译器可以移动左值返回值,但在某些情况下,它甚至可以通过完全避免创建中间返回值来做得更好(既不制作副本,也不执行移动)。在这种情况下,既不会调用拷贝构造函数,也不会调用移动构造函数。
禁用复制
在上面的Auto_ptr4类中,为了便于比较,我们保留了拷贝构造函数和赋值运算符。但在支持移动的类中,有时需要删除拷贝构造函数和拷贝赋值函数,以确保不会进行复制。对于Auto_ptr类,我们不想复制模板化对象T,一个原因是它可能很昂贵,而且类T可能也不支持复制!
下面是支持移动语义但不支持复制语义的Auto_ptr版本:
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
|
#include <iostream>
template<typename T>
class Auto_ptr5
{
T* m_ptr {};
public:
Auto_ptr5(T* ptr = nullptr)
: m_ptr { ptr }
{
}
~Auto_ptr5()
{
delete m_ptr;
}
// 拷贝构造 -- 不允许拷贝!
Auto_ptr5(const Auto_ptr5& a) = delete;
// 移动构造函数
// a.m_ptr 的所有权转移至 m_ptr
Auto_ptr5(Auto_ptr5&& a) noexcept
: m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
}
// 拷贝赋值 -- 不允许拷贝!
Auto_ptr5& operator=(const Auto_ptr5& a) = delete;
// 移动赋值函数
// a.m_ptr 的所有权转移至 m_ptr
Auto_ptr5& operator=(Auto_ptr5&& a) noexcept
{
// 自我赋值检查
if (&a == this)
return *this;
// 释放已经持有的资源
delete m_ptr;
// a.m_ptr 的所有权转移至 m_ptr
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
|
如果试图按值将Auto_ptr5左值传递给函数,编译器将报错,提示初始化函数参数所需的拷贝构造函数已被删除。这很好,因为无论如何,通常都应该通过常量左值引用传递Auto_ptr5!
Auto_ptr5是一个很好的智能指针类。事实上,标准库包含一个与此非常相似的类(您应该使用它),名为std::unique_ptr。我们将在本章后面继续讨论std::unique_ptr。
另一个例子
让我们再看另一个使用动态内存的类:一个简单的动态模板数组。此类包含深拷贝构造函数和拷贝赋值函数。
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
|
#include <algorithm> // for std::copy_n
#include <iostream>
template <typename T>
class DynamicArray
{
private:
T* m_array {};
int m_length {};
public:
DynamicArray(int length)
: m_array { new T[length] }, m_length { length }
{
}
~DynamicArray()
{
delete[] m_array;
}
// 拷贝构造
DynamicArray(const DynamicArray &arr)
: m_length { arr.m_length }
{
m_array = new T[m_length];
std::copy_n(arr.m_array, m_length, m_array); // 复制 m_length 个元素,从 arr 到 m_array
}
// 拷贝赋值
DynamicArray& operator=(const DynamicArray &arr)
{
if (&arr == this)
return *this;
delete[] m_array;
m_length = arr.m_length;
m_array = new T[m_length];
std::copy_n(arr.m_array, m_length, m_array); // 复制 m_length 个元素,从 arr 到 m_array
return *this;
}
int getLength() const { return m_length; }
T& operator[](int index) { return m_array[index]; }
const T& operator[](int index) const { return m_array[index]; }
};
|
现在让我们在程序中使用这个类。为了展示在堆上分配一百万个整数时这个类的运行表现,我们将利用之前开发的Timer类为代码计时。通过Timer类,我们可以测量代码运行速度,并展示复制和移动之间的性能差异。
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
|
#include <algorithm> // for std::copy_n
#include <chrono> // for std::chrono functions
#include <iostream>
// 使用上面的 DynamicArray 类
class Timer
{
private:
// 类型别名,简化代码
using Clock = std::chrono::high_resolution_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg { Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
// 返回一个所有元素都是输入双倍的数组
DynamicArray<int> cloneArrayAndDouble(const DynamicArray<int> &arr)
{
DynamicArray<int> dbl(arr.getLength());
for (int i = 0; i < arr.getLength(); ++i)
dbl[i] = arr[i] * 2;
return dbl;
}
int main()
{
Timer t;
DynamicArray<int> arr(1000000);
for (int i = 0; i < arr.getLength(); i++)
arr[i] = i;
arr = cloneArrayAndDouble(arr);
std::cout << t.elapsed();
}
|
在作者的一台机器上,在发布模式下,该程序在0.00825559秒内执行。
现在,让我们再次运行相同的程序,但将拷贝构造函数和拷贝赋值替换为移动构造函数和移动赋值。
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
|
template <typename T>
class DynamicArray
{
private:
T* m_array {};
int m_length {};
public:
DynamicArray(int length)
: m_array(new T[length]), m_length(length)
{
}
~DynamicArray()
{
delete[] m_array;
}
// 拷贝构造
DynamicArray(const DynamicArray &arr) = delete;
// 拷贝赋值
DynamicArray& operator=(const DynamicArray &arr) = delete;
// 移动构造
DynamicArray(DynamicArray &&arr) noexcept
: m_array(arr.m_array), m_length(arr.m_length)
{
arr.m_length = 0;
arr.m_array = nullptr;
}
// 移动赋值
DynamicArray& operator=(DynamicArray &&arr) noexcept
{
if (&arr == this)
return *this;
delete[] m_array;
m_length = arr.m_length;
m_array = arr.m_array;
arr.m_length = 0;
arr.m_array = nullptr;
return *this;
}
int getLength() const { return m_length; }
T& operator[](int index) { return m_array[index]; }
const T& operator[](int index) const { return m_array[index]; }
};
#include <iostream>
#include <chrono> // for std::chrono functions
class Timer
{
private:
// 类型别名,简化代码
using Clock = std::chrono::high_resolution_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg { Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
// 返回一个所有元素都是输入双倍的数组
DynamicArray<int> cloneArrayAndDouble(const DynamicArray<int> &arr)
{
DynamicArray<int> dbl(arr.getLength());
for (int i = 0; i < arr.getLength(); ++i)
dbl[i] = arr[i] * 2;
return dbl;
}
int main()
{
Timer t;
DynamicArray<int> arr(1000000);
for (int i = 0; i < arr.getLength(); i++)
arr[i] = i;
arr = cloneArrayAndDouble(arr);
std::cout << t.elapsed();
}
|
在同一台机器上,该程序在0.0056秒内执行。
比较两个程序的运行时间,(0.00825559-0.0056)/ 0.00825559 * 100=32.1%,性能提升了32.1%!
删除移动构造函数和移动赋值函数
可以使用=delete语法删除移动构造函数和移动赋值,就像删除拷贝构造函数和拷贝赋值一样。
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
|
#include <iostream>
#include <string>
#include <string_view>
class Name
{
private:
std::string m_name {};
public:
Name(std::string_view name) : m_name{ name }
{
}
Name(const Name& name) = delete;
Name& operator=(const Name& name) = delete;
Name(Name&& name) = delete;
Name& operator=(Name&& name) = delete;
const std::string& get() const { return m_name; }
};
int main()
{
Name n1{ "Alex" };
n1 = Name{ "Joe" }; // 错误: 移动赋值被删除
std::cout << n1.get() << '\n';
return 0;
}
|
如果删除拷贝构造函数,编译器将不会生成隐式移动构造函数(这会使对象既不可复制,也不可移动)。因此,在删除拷贝构造函数时,明确指定移动构造函数的行为是有用的。可以将它们设置为delete(明确这是所需行为),也可以设置为default(让类仅支持移动)。
如果您想要可复制但不可移动的对象,那么仅删除移动构造函数和移动赋值似乎是个好主意,但这会导致在复制省略不生效的情况下,类不能按值返回。发生这种情况是因为,虽然移动构造函数被声明为删除,但它仍会参与重载解析。函数按值返回时会优先匹配已删除的移动构造函数,而不是未删除的拷贝构造函数。下面的程序说明了这一点:
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
|
#include <iostream>
#include <string>
#include <string_view>
class Name
{
private:
std::string m_name {};
public:
Name(std::string_view name) : m_name{ name }
{
}
Name(const Name& name) = default;
Name& operator=(const Name& name) = default;
Name(Name&& name) = delete;
Name& operator=(Name&& name) = delete;
const std::string& get() const { return m_name; }
};
Name getJoe()
{
Name joe{ "Joe" };
return joe; // 错误: 移动构造函数被删除
}
int main()
{
Name n{ getJoe() };
std::cout << n.get() << '\n';
return 0;
}
|
关键洞察力
如果定义或删除了拷贝构造函数、拷贝赋值、移动构造函数、移动赋值或析构函数中的任意一个,那么也应该明确处理其余每个函数。
移动语义和std::swap的问题(进阶)
交换也适用于移动语义,这意味着我们可以通过将资源与即将被销毁的对象交换,来实现移动构造函数和移动赋值。
这有两个好处:
- 持久对象现在控制以前属于即将销毁对象的资源(这是我们的主要目标)。
- 即将销毁的对象现在控制以前属于持久对象的资源。当该对象真正销毁时,它可以清理这些资源。
当您考虑交换时,首先想到的通常是std::swap()。然而,使用std::swap()实现移动构造函数和移动赋值是有问题的,因为std::swap会导致无限递归。
您可以在以下示例中看到这种情况:
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
|
#include <iostream>
#include <string>
#include <string_view>
class Name
{
private:
std::string m_name {}; // std::string 是可移动的
public:
Name(std::string_view name) : m_name{ name }
{
}
Name(const Name& name) = delete;
Name& operator=(const Name& name) = delete;
Name(Name&& name) noexcept
{
std::cout << "Move ctor\n";
std::swap(*this, name); // 有问题!
}
Name& operator=(Name&& name) noexcept
{
std::cout << "Move assign\n";
std::swap(*this, name); // 有问题!
return *this;
}
const std::string& get() const { return m_name; }
};
int main()
{
Name n1{ "Alex" };
n1 = Name{"Joe"}; // 触发移动赋值
std::cout << n1.get() << '\n';
return 0;
}
|
这将打印:
1
2
3
4
5
|
Move assign
Move ctor
Move ctor
Move ctor
Move ctor
|
无限打印……直到堆栈溢出。
而通过交换成员,就可以简单地实现移动语义。下面是一个示例:
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
|
#include <iostream>
#include <string>
#include <string_view>
class Name
{
private:
std::string m_name {};
public:
Name(std::string_view name) : m_name{ name }
{
}
Name(const Name& name) = delete;
Name& operator=(const Name& name) = delete;
// 交换函数,交换Name的成员
friend void swap(Name& a, Name& b) noexcept
{
// 通过 std::swap 交换成员, 而不是整个对象,
// 避免无限循环
std::swap(a.m_name, b.m_name);
}
Name(Name&& name) noexcept
{
std::cout << "Move ctor\n";
swap(*this, name); // 调用我们自己的 swap, 而不是 std::swap
}
Name& operator=(Name&& name) noexcept
{
std::cout << "Move assign\n";
swap(*this, name); // 调用我们自己的 swap, 而不是 std::swap
return *this;
}
const std::string& get() const { return m_name; }
};
int main()
{
Name n1{ "Alex" };
n1 = Name{"Joe"}; // 触发移动赋值
std::cout << n1.get() << '\n';
return 0;
}
|
这按预期工作,并打印: