- 本节内容非常重要,讲解C++对象的构造、赋值和析构函数的自动生成行为,以及你应以什么规则显式声明它们。
- 特殊成员函数(special member functions) 指C++在类中会自动生成的函数。
- C++98中有4个这样的函数:默认构造函数(default constructor),析构函数(destructor),复制构造函数(copy constructor)和复制赋值运算符(copy assignment operator)。只有当类中没有定义但有调用时,它们才会被生成。其中默认构造函数只有当类没有声明任何构造函数时才会被生成。生成的函数默认是
public
且inline
的。它们也是非虚函数,除了一种情况:继承自有虚析构函数的基类的派生类,生成的析构函数也是虚函数。(笔者注:对这些内容还不熟悉的,推荐阅读一下《深度探索C++对象模型》第2章 构造函数语义学) - C++11迎来了两位新成员:移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。大原则还是相同:需要时才生成。它们的行为与一对复制函数相似,也是将类的非静态成员“逐成员地(memberwise)移动”。不同点在于,这里的行为更应该说是“尽量”这样做——每个成员如果能够被移动构造,那么就移动构造;如果不能移动构造,那就复制构造。注意这里的“不能移动构造”指的是没有显式声明移动构造/赋值函数,而非通过
delete
主动屏蔽了移动构造函数,绝大多数来自C++98的类都如此。如以下代码所示:
// 这里重点讨论的是调用问题,不要在意函数实现
class Movable {
public:
Movable() {}
Movable(Movable&& rhs) { cout << "Movable move constructor called." << '\n'; }
Movable& operator=(Movable&& rhs) {
cout << "Movable move assignment operator called." << '\n';
return *this;
}
};
class UnMovable {
public:
UnMovable() {}
UnMovable(const UnMovable& rhs) { cout << "Unmovable copy constructor called" << '\n'; }
UnMovable& operator=(const UnMovable& rhs) {
cout << "Unmovable copy assignment operator called" << '\n';
return *this;
}
// 如果声明了以下函数,Widget的移动构造会报错
// UnMovable(UnMovable&& rhs) = delete;
// UnMovable& operator=(UnMovable&& rhs) = delete;
};
class Widget {
private:
Movable m;
UnMovable um;
};
int main()
{
Widget w1;
Widget w2(std::move(w1));
// 注意 Widget w3 = std::move(w2) 与 Widget w3(std::move(w2))
// 同样调用move constructor而非move assignment operator
Widget w3;
w3 = std::move(w2);
return 0;
}
- 移动构造/赋值函数与复制函数的生成条件有一点不同:对于复制,两个函数是独立的,即如果声明了复制构造但调用复制赋值,则编译器会自动生成复制赋值函数,反之亦然(这种特性来自C++98,在11中已被视为deprecated,原因见下文)。而移动函数不是独立的。只要声明了一个,另一个就不会被自动生成,如果调用会导致编译错误。
Movable m1, m2; // Movable仅有移动构造函数
m2 = m1; // error! 不会自动生成移动赋值函数
UnMovable um1, um2; // Unmovable仅有复制构造函数
um2 = um1; // ok, 自动生成复制赋值函数
- 更进一步地,只要类显式声明了一个复制函数,那么就不会自动生成移动函数,反之亦成立。其中的道理在于,如果默认生成的 memberwise 的复制/构造函数中的某一个不能满足要求,那么其它几个大概也是不正确的。最典型的例子就是当要在类内管理某种资源(指针)时,如果有必要在一个复制函数中进行某种操作(如指针内容复制memcpy或strcpy)时,那么几乎肯定另一个也要进行操作,而且析构函数中也要进行操作(一般是释放资源)。这就是C++98中著名的 Rule of Three,标准库中涉及内存管理的类(如STL中的容器类)都会同时声明这三个函数。加上C++11的移动构造后,可以拓展为零/三/五原则:要么不声明这五个函数中的任意一个(默认行为足够),要么声明两复制+析构三个函数(移动构造时会有性能损失,但逻辑ok,兼容C++98的类),要么声明两复制+两移动+析构五个函数。
- 回到本节的主题,由此可以引出移动函数被自动生成的条件:
- 类中没有声明复制操作。
- 类中没有声明移动操作。
- 类中没有声明析构函数。
- 为了向后兼容,复制函数没有采取这么严格的要求,即已声明析构或构造/赋值之一函数时,还是可以自动生成另一个赋值/构造函数(所以 Rule of Three 是idiom而非强制),但这在C++11中被视为废弃(deprecated)特性。
- 如果你已经声明了其中之一,但认为其它函数自动生成的版本是正确的,你可以在声明它们时用
= default
标识。这种方法在多态基类中很有用,因为多态基类应该使用 virtual destructor(推荐阅读《Effective C++》Item 07),只能显式声明。此时默认的复制、移动操作可能仍是正确的,但显式声明析构函数导致移动函数不能自动生成;显式声明了移动函数又导致复制函数不能自动生成。此时就可以用= default
对这五个函数进行显式声明。
class Base {
public:
Base(const Base&) = default;
Base& operator=(const Base&) = default;
Base(Base&&) = default;
Base& operator=(Base&&) = default;
virtual ~Base() = default;
};
- 甚至就算编译器本来会自动生成的函数,你也可以通过这种方式来更清晰地表达你的语义。
- 小结:C++11中特殊函数的规则
- 默认构造函数(default constructor):与C++98规则相同。当类没有用户声明的任意构造函数时被生成。
- 析构函数(destructor):基本与C++98规则相同,唯一区别是其默认为
noexcept
。同C++98,只有当基类析构函数是 virtual 时才是 virtual。 - 复制构造函数(copy constructor):运行时行为与C++98相同:对非static成员进行memberwise复制。当类中没有用户声明的复制构造函数时被生成。如果声明了移动函数(之一)则被删除(deleted)。复制赋值运算符或析构函数存在时仍生成该函数的特性已被废弃(deprecated)。
- 复制赋值运算符(copy assignment operator):运行时行为与C++98相同:对非static成员进行memberwise复制。当类中没有用户声明的复制赋值运算符时被生成。如果声明了移动函数(之一)则被删除(deleted)。复制构造函数或析构函数存在时仍生成该函数的特性已被废弃(deprecated)。
- 移动构造函数(move constructor)和移动赋值运算符(move assignment operator):都对非static成员进行memberwise的移动。当类中没有用户声明的复制函数、移动函数和析构函数时被生成。
- 一个小point:可能推导出与复制或移动函数形式相同的模板函数不会阻止复制或移动函数的自动生成。例如下面的函数当传入类型
T
为Widget
时签名与复制构造/赋值函数相同,但不会影响它们的自动生成。
class Widget {
public:
...
template<typename T>
Widget(const T& rhs);
template<typename T>
Widget& operator=(const T& rhs);
...
};
总结
- 特殊成员函数是编译器可能自动生成的函数:默认构造函数、析构函数、两个复制函数、两个移动函数。
- 第二条和第三条,见上面的总结。
- 模板函数不会阻止特殊函数的生成。