特殊成员函数
特殊成员函数是一种约定俗成的说法,一般指编译器会自行生成的那些预置成员函数。这些函数包括默认构造函数、析构函数、复制/移动构造函数和复制/移动赋值运算符重载。
此外,还包括预置比较运算符。
讨论这些预置函数生成的时机是很复杂的。首先我列出下面的表格:这包括了之前在第五章讨论过的默认构造和复制构造/赋值重载。
函数 | 何时隐式预置 | 何时弃置(=delete )1 |
---|---|---|
默认构造函数 | 用户未声明任何构造函数 | |
析构函数 | 用户未声明析构函数 | |
复制构造函数 | 用户未声明 三之原则 2 之一 | 用户声明了“移动语义” |
复制赋值重载 | 用户未声明 三之原则 2 之一 | 用户声明了“移动语义” |
移动构造函数 | 用户未声明 五之原则 3 之一 | |
移动赋值重载 | 用户未声明 五之原则 3 之一 | |
operator<=> | ||
operator== | operator<=> 是预置的 |
- 这一列总是包括基类或成员对应特殊函数被弃置或不可访问。
- 三之原则:析构函数、复制构造、复制赋值。
- 这与第五章提到的规则有所出入。事实是,若用户声明了析构函数或复制赋值,编译器仍会预置复制构造函数,但这是被弃用的语法;
- 同理,若用户声明了析构函数或复制构造,编译器仍会预置复制赋值重载,但这是被弃用的语法。
- 总之,若用户声明了 三之原则 之一的函数,则编译器要么不会预置,要么预置但不推荐使用。
- 五之原则:析构函数、复制构造、复制赋值、移动构造、移动赋值。
其中 operator<=>
和 operator==
的预置是之后章节要讲的,现在先不用管。表格前四行是第五章已经了解过的,只是这里用“三之原则”概括性地总结了。那么剩下的就是新鲜内容了:一是增加了因用户定义移动语义而弃置复制构造/赋值,二是预置的移动构造/赋值。
首先来了解预置的移动构造和移动赋值都做了什么。对于移动构造,隐式生成的函数体就是将实参中的各个基类和成员移动构造当前对象的各个基类和成员。对于移动赋值,同理移动赋值各个基类和成员。在代码上,就体现为 std::move
已有对象的成员,使得 operator=
或构造函数调用接收右值的重载。
struct A {};
struct B {
int a;
A b;
// [...]
// 下为编译器预置的移动构造函数
B(B&& b): a{std::move(b.a)}, b{std::move(b.b)} /* [...] */ {}
// 下为编译器预置的移动赋值重载
B& operator=(B&& rhs) {
a = std::move(rhs.a);
b = std::move(rhs.b);
// [...]
return *this;
}
}
需要注意的是,预置移动构造/赋值是非常平凡的。它的定义和我们之前的 String
或 UniquePtr
都不一样——这两个的移动是不平凡的,它们需要进行所有权转移。而平凡的移动构造不会帮我们做这件事。如果我们的 String
或 UniquePtr
用了预置的移动构造/赋值,那就会导致如“浅拷贝”、所有权不唯一等错误。这些预置函数的作用仅仅是保证成员或基类的移动语义。比如:
#include <memory>
#include <string>
struct S {
// std::string 和 std::unique_ptr 都拥有移动语义
std::string a;
std::unique_ptr<int> b;
// 那么下面这些预置函数保证:
// S 类型对象被移动时,成员 a 和 b 也被正确移动
S(S&&) = default;
S& operator=(S&&) = default;
}
那么什么时候编译器会帮我们生成这些预置函数的定义呢?首先第一个前提是所有成员都可以被移动,其次就是用户没有手动定义“五之原则”中的任何一个函数。五之原则(The rule of 5)是这样说的:如果这个类包含移动语义,那么它通常需要自己定义复制构造函数(可能是弃置的)、复制赋值重载(可能是弃置的)、移动构造函数、移动赋值重载和析构函数。
不同于三之原则,编译器会近乎强制要求你遵循五之原则。如果你声明了五之原则中的任意一个函数,那么其余四个都不会预置,从而移动语义无法被正确实现。你必须手动地、显式地指明剩余四个是如何移动、复制或者弃置的。总之,这也保证了你的类的所有权管理是妥当的,尽可能避免内存泄漏和不必要的复制开销。
总结
我们最后总结一下移动语义与右值引用相关的语法。由于右值对象总是保持极短的生命周期,为了利用其中的资源,从而设计了接受右值引用的移动构造函数和移动复制重载。移动构造和移动赋值使用 T&&
作为形参类型,即仅接受右值的构造,可以从这个短命对象中“窃取”相关资源。一个左值对象可以通过 std::move
转换为右值对象,从而让对方转移走自己的资源。除了移动构造和移动赋值外,没有什么地方会用到右值引用了。
此外,如果一个模板函数中,T
是被推导的类型形参,那么 T&&
类型的形参是转发引用,可以通过 std::forward<T>
保持传入实参的值类别。