作为totw#131最初发表于2017年3月24日
由James Dennett (jdennett@google.com)创作
从一开始,C++ 就支持一些所谓的特殊成员函数的编译器声明版本:默认构造函数、析构函数、复制构造函数和复制赋值运算符。 C++11 向列表添加了移动构造和移动赋值,并添加了语法(=default 和 =delete)以控制何时声明和定义这些默认值。
=default 有什么作用,我们为什么要使用它?
写 =default 是我们告诉编译器“你通常会为这个特殊成员函数做的事情”的方式。 为什么我们要这样做而不是手动编写实现或让编译器为我们声明一个?
- 我们可以更改访问级别(例如,使构造函数受保护而不是公共),使析构函数为虚拟,或恢复将被抑制的函数(例如,具有其他用户声明的构造函数的类的默认构造函数)并且仍然 让编译器为我们生成函数。
- 如果复制/移动成员就足够了,编译器定义的复制和移动操作不需要在每次添加或删除成员时进行维护。
- 编译器提供的特殊成员函数可以是不重要的(当它们调用的所有对自身的操作是不重要的),这可以使它们更快、更安全。
- 具有默认构造函数的类型可以是聚合的,因此支持聚合初始化,而具有用户提供的构造函数的类型则不能。
- 显式声明一个默认成员为我们提供了一个地方,用来记录结果函数语义。
- 在类模板中,=default 是一种简单方法,去有条件地声明操作,具体取决于某些基础类型是否提供它。
当我们在特殊成员函数的初始声明中使用 =default 时,编译器将检查它是否可以为该函数合成为内联定义。 如果可以,它就进行。 如果不能,该函数实际上被声明为已删除,就像我们写了 =delete 一样。 这正是我们透明地包装类所需要的(例如,如果我们正在定义一个类模板),但读者可能会感到惊讶。
如果函数的初始声明使用 =default,或者如果编译器声明了一个非用户声明的特殊成员函数,则会推导出适当的 noexcept 规范,从而可能允许更快的代码。
它是如何工作的呢?
在C++11之前,如果我们需要一个默认的构造函数或已经有了其他的构造函数,那么我们可以这样写:
class A {
public:
A() {} // User-provided, non-trivial constructor makes A a non-aggregate.
};
从C++11开始我们有更多选择:
class C {
public:
C() = default; // misleading: C has a deleted default constructor
private:
const int i; // const => must always be initialized.
};
class D {
public:
D() = default; // unsurprising, but not explicit: D has a default constructor
private:
std::unique_ptr<int> p; // std::unique_ptr has a default constructor
};
显然,我们不应该编写类 C 之类的代码:在非模板中,仅当您希望该类支持该操作(然后测试它是否支持)时才使用 =default。 clang-tidy 包括对此的检查。
当在特殊成员函数的第一次声明之后(即在类之外)使用 =default 时,它具有更简单的含义:它告诉编译器定义函数,并在无法这样做时给出错误.当在类外使用 =default 时,默认函数将不是微不足道的:微不足道由第一个声明确定(因此所有客户端都同意操作是否微不足道)。
如果你不需要你的类是一个聚合并且你不需要构造函数是微不足道的,那么在类定义之外默认构造函数,比如下面的示例 E 和 F,通常是一个不错的选择。它的含义对读者来说是清楚的,并由编译器检查。对于默认构造函数或析构函数的特殊情况,我们可以写 {} 而不是 =default,但对于其他默认操作,编译器生成的实现不那么简单,为了保持一致性,最好在所有适用情况下写 =default。
class E {
public:
E(); // promises to have a default constructor, but...
private:
const int i; // const => must always be initialized.
};
inline E::E() = default; // compilation error here: would not initialize `i`
class F {
public:
F(); // promises to have a default constructor
private:
std::unique_ptr<int> p; // std::unique_ptr has a default constructor
};
inline F::F() = default; // works as expected
建议
优先 =default 而不是手动编写等效的实现,即使该实现只是 {}。 可选地,从初始声明中省略 =default 并提供单独的默认实现。
注意默认的移动操作。 来自移动的对象仍然必须满足其类型的不变量,并且默认实现通常不会保留字段之间的关系。
在模板之外,如果 =default 不提供实现,则改为使用 =delete。