根据c++官方说法,特殊成员函数是c++自己生成的。c++98有4个,构造函数,析构函数,拷贝构造函数,赋值拷贝函数。这些函数只是在需要的时候才会生成。比如一些代码调用了这些函数,而之前又没有在类中事前定义这些函数。构造函数只有在你没有自己定义一个构造函数情况下才会自动生成出来。(当你自定义了有参数的构造函数时,阻止了编译器为你自动生成默认构造函数)。这些新生成的函数都是public和inline类型。一般也不是虚函数,只有派生类的析构函数,在基类析构函数是虚函数情况下,才是虚函数。
但你已经知道这些事情了。是的,是的,远古史前时代,美索不达米亚,商朝,FORTAN,以及c++98.然而时代变了,之前的特殊函数生成规则变了。知晓这些新规则很重要。没有哪些事情比懂得编译器如何静悄悄的给你的类中插入成员函数,更能有效编程了。
在c++11, 这个特殊成员函数俱乐部又增加了两个,move和move赋值函数,签名如下
class Widget {
public:
Widget(Widget &&rhs);
Widget& operator=(Widget&&rhs);
...
};
它们的生成和行为规则和它们的拷贝兄弟相差无几。move函数旨在需要时候才生成,会对每个非静态数据成员move。这也意味着会对来自参数中的数据成员move操作。move也会对基类操作(如果存在的话)。
现在当我提到对一个数据成员或者基类move时,并不确保实际的move操作一定会发生。“按成员逐一move”,实际上更像按成员发出move请求,有些类型(大部分c++98传统的类)并不支持move,那么会通过拷贝来完成所谓move。简单记住,按成员move,就是move可以move的成员以及支持move的基类。这和拷贝构造函数不同。
和拷贝构造函数一样,如果你自定义了一个move构造函数,那么就不会再自动生成一个。然而move构造函数自动生成满足的条件和拷贝构造函数有一点不同。
另个拷贝构造函数是独立的,自定义了一个,并不会阻止编译器自动生成出另外一个。比如你只定义了拷贝构造函数,没有定义拷贝赋值函数,当你代码中调用拷贝赋值函数时候,编译器会自动帮你生成出一个。这在c++98中是对的,在c++11里仍然正确。
两个move函数并不是彼此独立的。如果你定义了其中一个,便会阻止编译器生成另外一个。这种做法的原因是:如果你自己定义了一个,意味着默认的逐成员move策略不适用了,那么应用这个策略的move赋值函数也就不适用了。
还有一点,如果自定义了拷贝函数,那么move函数也不会自动生成。因为自定义拷贝函数,意味着正常的拷贝操作(逐成员拷贝)不再适用,那么逐成员的move也就必然也不再适用了。
反过来也成立,如果自定义了一个move函数,那么拷贝函数也不会自动生成了。一句话,如果逐成员move不适用了,那么逐成员复制也别希望能用。着看起来打破了C++98的规则,但c++98没有move,所以不会有问题。
也许你听到过三合一规则,也就说如果定义了拷贝,赋值和析构函数里面的一个,那么其余的也需要自定义。这主要与资源管理有关,如果在一个函数中需要特殊管理,那么在其他函数中可能也需要特殊操作。典型的资源是内存,这也是为什么STL容器实施动态内存管理时,都定义了“三剑客”:两个拷贝和一个析构。
自定义了析构函数,意味着拷贝操作中的逐成员拷贝可能不再适用了。这也意味着,如果自定义了析构函数,那么拷贝函数最好不要自动生成,因为自动生成的可能会功能不正常。在c++98时代,没有充分重视这个问题。因此自定义析构函数不影响编译器产生拷贝函数。c++11里这个规则也成立,只不过不想打破太多传统代码。
三合一准则仍然成立。定义拷贝函数或者析构函数,都会阻止move函数的自动生成。自动生成move必须满足下列条件:
没有拷贝函数声明
没有move函数声明
没有析构函数声明
从某种角度上讲,相同的规则可以扩展到拷贝操作上,因为c++11不鼓励自定义拷贝或析构函数时,自动生成拷贝函数。这意味着如果你有一个自定义析构函数或者拷贝函数的类,你又依赖了编译器自动生成的拷贝构造函数,你就最好升级代码,取消这种依赖。如果逐成员拷贝策略时正确的,工作就简单了,因为c++11提供了=default来表明它。
class Widget {
public:
Widget(const Widget &rhs) = default;
Widget& operator=(const Widget&rhs) = default;
...
};
这个方法在有多态基类时有用。比如基类定义了一些接口,来管理派生类。多态基类通常析构函数是虚函数,因为如果不是虚函数,在基类指针指向派生类对象时,对基类指针的delete,typeid操作就会导致未定义行为。要将析构函数弄成虚函数,除了继承有虚析构函数的基类外,就只能显示声明虚拟析构函数了。通常默认实现就可以了,使用=default是一种很好的表达方式。但因为自定义析构函数会阻止move函数自动生成。所以为了支持move操作,要再用上=default。自定义move会阻止copy,为了支持copy,再来一轮=default。
class Widget {
public:
virtual ~Widget() = default;
Widget(Widget &&rhs) = default;
Widget& operator=(Widget&&rhs) = default;
Widget(const Widget &rhs) = default;
Widget& operator=(const Widget&rhs) = default;
...
};