17.了解自动生成的特殊成员函数

前言

 
按照官方说法,所谓特殊成员函数就是C++自动生成的函数。在C++98中有4种这样的函数(它们自动生成的条件可见Inside the C++ Object Model):

  1. default constructor
  2. default destructor
  3. default copy constructor
  4. default copy assignment operator

一般而言,自动生成的此类特殊函数将隐式地具备public && inline,并且为non-virtual(析构函数存在一个意外,如果该类继承自一个具备virtual destructor的基类,则自动生成的default destructor具备virtual属性)。

上述内容你可能已经完全掌握,但在C++11中它们发生了一些变化,了解这些新的规则对C++高效编程至关重要。


move constructor and move assignment

 
C++11的特殊成员函数相较于C++98多出了两个新人:move constructor与move assignment,它们的签名如下所示:

1
2
3
4
5
6
7
class Widget {
public:

Widget(Widget&& rhs); // move constructor
Widget& operator=(Widget&& rhs); // move assignment operator

};

生成它们的规则类似于copy,move操作仅在它们需要时才被生成,其行为是对rhs的所有non-static data member执行move操作,此外,它们对rhs中的基类部分同样执行move操作。

严格意义上来说,move constructor与move assignment更像是一种请求,我们无法保证必然会发生move行为。这种原因很简单,C++98中大量的历史遗留类型并不支持move操作,,因此它们实际上通过copy操作完成了move行为。move行为的核心操作是对所有需要移动的数据执行std::move,并且在函数重载期内根据实际情况判断到底是使用copy还是move(Item23 将会对此进行详尽分析)。我们现在所需要了解的就是对所有能move的执行move,不能move的执行copy。


move操作的生成

 
类似于copy,自动生成move操作仅发生于你并未手动地声明它,但其生成条件和copy相比还是略有不同。

两个copy操作相互独立,也就是说声明其中的一个对另一个并无任何影响。如果你声明了一个copy constructor但没有声明copy assignment,那么编译器将会在需要的时候自动定义与声明copy assignment,反之亦是如此。

两个move操作并不独立,其中的任何一个被声明都会阻止编译器生成另外一个。理由很简单,如果你声明了一个移动构造函数,那你至少应当指出如何实现移动构造以及它与编译器默认memberwise move版本的不同之处。如果 memberwise move constructor并不适用于该类,那么几乎必然memberwise move assignment也不适用。同理,声明move assignment也会阻止move constructor的生成。

此外,move操作不会在任何显式声明了copy操作的类中生成。声明copy操作的原因是因为memberwise copy并不适用于该类,因此编译器会认为既然memberwise copy并不适合,那么memberwise move必然也不适合该类。这引发了另一个话题,如果一个类中声明了move操作,那么编译器将禁止自动生成copy操作,原因在于memberwise move无法适用的类想必也无法使用memberwise copy。这听起来可能会破坏C++98中的旧有程序,因为C++11中自动生成copy操作的条件似乎更加苛刻。但实则并非如此,C++98代码中不会出现move操作,因为C++98根本就不存在“move object”。遗留代码只有在经过符合C++11标准的修改之后才会具备移动语义。


三/五法则(C++ Primer P477)

 
也许你曾经听说过一条被称为“Rule of Three”的指导方针,该指导方针规定,copy constructor,copy assignment opertaor或destructor的声明总应该一起出现,不允许出现任何遗漏。该原则的背景在于,手动声明copy操作几乎必然是因为当前类负责资源管理,而资源管理类又存在如下特征:

  1. copy操作几乎不可能只使用一个(copy constructor中的行为也需要被应用于copy assignment opertaor之中)。
  2. 析构函数负责资源的释放。

一般来说需要管理的资源都是指内存,这也就是为何所有负责内存管理的标准库类均声明有这三件套的原因。

自定义destructor表明单纯的memberwise copy行为可能并不适用于该类,因此一旦存在用户自定义destructor,编译器将不会自发生成任何copy操作。在C++98被创建时这条规则尚未得到清楚认知,因此在C++98中用户自定义destructor并不会对编译器生成copy操作造成任何影响,在C++11中自定义destructor仍然不会限制copy操作的生成,但这次并非认知不明,而是出于担心破坏历史遗留代码的原因。

但“Rule of Three”规则依然有效,前文中我们已经明确带有自定义copy操作的类不会自动生成move操作,因此我们也可以推断,带有自定义destructor的class使用默认move操作也是不正确的行为(因为自定义destructor的存在表示当前类需要自定义copy操作)。因此,只有当下述三个条件成立时才会生成move操作:

  1. class中没有任何自定义copy操作。
  2. class中不存在另一个move操作的声明。
  3. class中不存在自定义destructor。

=default声明

 
在某些情况下,类似的规则可能会延伸至copy操作,原因在于C++11很是抵制为含有自定义destructor或copy操作的class生成copy操作。因此如果你曾经依赖于使用编译器自动生成的特殊成员函数,你应当仔细检查并按照“Rule of Three”法则消去其相关性。如果你认为memberwise足以完成工作,那你可以在声明时直接以default注明:

1
2
3
4
5
6
7
8
9
class Widget {
public:

~Widget(); // user-declared dtor

Widget(const Widget&) = default; // default copy ctor behavior is OK
Widget& operator=(const Widget&) = default;

};

=default在多态基类中使用非常普遍。多态基类通常具有虚析构函数,因为如果它们不存在,则某些操作(例如通过基类指针或引用对派生类对象使用delete或typeid)会产生未定义或误导性的结果。除非一个类已经继承了其基类的虚析构函数,否则你最好显式地声明它。前文已述,自定义destructor会抑制move操作的生成,因此你必须使用=default显式说明自己需要memberwise copy。类似地,声明移动操作会禁用copy操作,因此如果需要复制性的话,则需要手动声明copy操作为=default
1
2
3
4
5
6
7
8
9
class Base {
public:
virtual ~Base() = default; // make dtor virtual
Base(Base&&) = default; // support moving
Base& operator=(Base&&) = default;
Base(const Base&) = default; // support copying
Base& operator=(const Base&) = default;

};

此外,结果编译器愿意生成某些操作,也不妨碍我们在类中以=default显式地声明它们,尽管多打了几个字,但可以使接口更加清晰,同时避免一些微妙的错误。举例而言,当前存在一个表示字符串表的类,即一个允许通过整数ID快速查找字符串值的数据结构:
1
2
3
4
5
6
7
8
class StringTable {
public:
StringTable() {}
// functions for insertion, erasure, lookup,
// etc., but no copy/move/dtor functionality
private:
std::map<int, std::string> values;
};

可以看到该类并未声明copy操作、move操作等等,编译器将会在其需要时将其合成。但考虑如果你日后发现需要在构造和析构对象时做一个日志记录,例如:
1
2
3
4
5
6
7
8
class StringTable {
public:
StringTable() { makeLogEntry("Creating StringTable object"); } // added
~StringTable() { makeLogEntry("Destroying StringTable object"); } // added
// other funcs as before
private:
std::map<int, std::string> values; // as before
};

析构函数的加入看起来没毛病,但实际上它导致了移动操作不再会被生成,但同时复制操作不受影响。因此,在我们启动移动操作时,实际上使用的copy操作,而我们却对此一无所知(赋值map造成了相当严重的性能下降,这一切只是因为引入了一个自定义destructor)。如果我们在之前曾经手动声明过=default,那一切都不会发生问题。


C++11中特殊函数的规则总结

  • Default constructor
    和C++98一样,仅在不存在自定义constructor时自动生成。
  • Destructor
    类似于C++98,唯一的区别在于编译器自动生成的destructor带有noexcept属性,并且与C++98一致的是,仅有基类destructor为virtual的情况下才会具备virtual属性。
  • Copy constructor
    与C++98行为相同:复制构造所有的non-static data member。仅在class中不存在自定义copy constructor的情况下才会被生成。如果类中声明了移动操作,则不会生成copy constructor。不推荐在任何具备自定义copy操作或自定义destructor的class中依赖此函数。
  • Copy assignment operator
    类似于copy constructor。
  • Move constructor and move assignment operator
    为每一个non-static data member执行move操作。仅在类不包含自定义copy操作,move操作或析构函数时生成。

规则中并没有任何关于模板成员函数阻止其他特殊成员函数生成的说明,因此,如果有一个Widget class如下所示:

1
2
3
4
5
6
7
8
class Widget {

template<typename T>
Widget(const T& rhs); // construct Widget from anything
template<typename T>
Widget& operator=(const T& rhs); // assign Widget from anything

};

编译器仍将为Widget生成copy和move操作(假设其生成条件已经具备),即使存在可以实例化生成copy操作与move操作的成员函数模板(当T为Widget时)。在某些情况下这种行为可能会产生相当严重的后果,我们将Item26中对其进行详细论述。