EMCPP条款17:理解特种成员函数的生成机制

YiQi 管理员

在 C++ 官方用语中,特种成员函数是指那些 C++ 会自行生成的成员函数。

在 C++11 中,特种成员函数俱乐部中加入了两位新会员:移动构造由数和移动赋值运算符,它们的签名如下:

1
2
3
4
5
class Widget {
public:
Widget(Widget&& rhs); // move constructor
Widget& operator=(Widget&& rhs); // move assignment operator
};

这两个特种成员函数的生成规则和行为表现一如其复制版本。移动操作也仅在需要时才生成,而一且生成,它们执行的也是作用千非静态成员的”按成员移动”操作。意思是,移动构造函数将依照其形参 rhs 的各个非静态成员对于本类的对应成员执行移动构造,而移动赋值运算符则将依照其形参 rhs 的各个非静态成员对于本类的对应成员执行移动赋值。移动构造函数同时还会移动构造它的基类部分(如果有的话),而移动赋值运算符则会移动赋值它的基类部分。

按成员移动是由两部分组成的,一部分是在支持移动操作的成员上执行的移动操作,另一部分是在不支持移动操作的成员上执行的复制操作。

两种复制操作是彼此独立的:声明了其中一个,并不会阻止编译器生成另一个。两种移动操作并不彼此独立:声明了其中一个,就会阻止编译器生成另一个。这种机制的理由在于,假设你声明了一个移动构造函数,你实际上表明移动操作的实现方式将会与编译器生成的默认按成员移动的移动构造函数多少有些不同。而若是按成员进行的移动构造操作有不合用之处的话,那么按成员进行的移动赋值运算符极有可能也会有不合用之处。

犹有进者,一且显式声明了复制操作,这个类也就不再会生成移动操作了。这样的判断的依据在于,声明复制操作(无论是复制构造还是复制赋值)的行为表明了对象的常规复制途径(按成员复制)对于该类并不适用。编译器从而判定,既然按成员复制不适用于复制操作,则按成员移动极有可能也不适用于移动操作。反之亦然。 一且声明了移动操作(无论是移动构造还是移动赋值),编译器就会废除复制操作(废除的方式是删除它们,参见条款 11)。毕竟如果按成员移动不是对象认为适当的移动方式的话,也就没有理由期望按成员复制是对象认为适当的复制方式。

你可能听说过一条指导原则叫做大三律 (Rule of Three)。大三律是说,如果你声明了复制构造函数、复制赋值运算符,或析构函数中的任何一个,你就得同时声明所有这三个。

由于大三律背后的理由仍然成立,再结合声明了复制操作就会阻止隐式生成移动操作的事实,就推动了 C++11 中的这样一个规定:只要用户声明了析构函数,就不会生成移动操作。

这么一来,移动操作的生成条件(如果需要生成)仅当以下三者同时成立:

  • 该类未声明任何复制操作。
  • 该类未声明任何移动操作。
  • 该类未声明任何析构函数。

请注意,这些机制中只字未提成员函数模板的存在会阻止编译器生成任何特种成员函数。这就意味着,如果 Widget 形如:

1
2
3
4
5
6
class Widget {
template<typename T> // construct Widget
Widget(const T& rhs); // from anything
template<typename T> // assign Widget
Widget& operator=(const T& rhs); // from anything
};

编译器会始终生成 Widget 的复制和移动操作(假定支配其生成的条件都得到了满足),即使这些模板的具现结果生成了复制构造函数或复制赋值运算符的签名(当 T 的值为 Widget 时就会发生这种情况)。条款26会告诉你,这么一个边缘场景有着至关重要的推论。

要点速记

  • 特种成员函数是指那些 C++ 会自行生成的成员函数。默认构造函数、析构函数、复制操作,以及移动操作。
  • 移动操作仅当类中未包含用户显式声明的复制操作、移动操作和析构函数时才生成。
  • 复制构造函数仅当类中不包含用户显式声明的复制构造函数时才生成,如果该类声明了移动操作则复制构造函数将被删除 。 复制赋值运算符仅当类中不包含用户显式声明的复制赋值运算符才生成,如果该类声明了移动操作则复制赋值运算符将被删除。在已经存在显式声明的析构函数的条件下,生成复制操作已经成为了被废弃的行为 。
  • 成员函数模板在任何情况下都不会抑制特种成员函数的生成。
此页目录
EMCPP条款17:理解特种成员函数的生成机制