C++中有6种特殊的成员函数:默认构造函数、析构函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符。
这些成员函数在一些情况下会由编译器自动生成,并且都是public的。那么,哪些情况会阻碍这些成员函数的生成呢?
几个例子
案例1
下面的代码能否编译通过?
class NoCopy {
public:
NoCopy() = default;
NoCopy(NoCopy&& rhs) = default; // 移动构造函数
};
int main() {
NoCopy a;
NoCopy b(a); // ERROR
}
答案是不能,编译器给出的错误是:
error: use of deleted function 'constexpr NoCopy::NoCopy(const NoCopy&)
意思是说,NoCopy类没有复制构造函数,所以不能用a来初始化b。
如果删掉移动构造函数的声明,则代码可以正常编译。
这是为什么?其实就是因为,移动构造函数的声明会导致编译期不会隐式生成复制构造函数。
案例2
我们再看一个例子,下面这个代码,调用的是移动构造函数,还是复制构造函数?
class NoMove {
public:
NoMove() = default;
virtual ~NoMove() = default; // 显式声明析构函数
private:
std::string s;
};
int main() {
NoMove a;
NoMove b(std::move(a)); // 能否调用移动构造函数?
}
答案是,调用的是复制构造函数。原因和上例差不多,这里是因为显式声明了析构函数,导致不会隐式生成移动构造函数。在需要移动构造函数时,会使用复制构造函数来代替。
你可能会想,反正都是编译器自动生成的,有必要区分吗?
是的,有必要,这决定了会调用其成员变量s的移动构造函数还是复制构造函数。
自动生成规则
看了上面的例子,会有个疑问,特种成员函数在什么情况下不会自动生成呢?
默认构造函数大家应该都知道,只有没有声明任何构造函数的情况下才会生成。
其他5个成员函数的生成更加复杂,它们之间会相互影响。
如图,红色箭头代表显示声明一个成员函数会导致另一个成员函数不会自动生成;绿色代表没有影响:
为什么会有如此复杂的关系?一方面是考虑“大三律”的思想,另一方面是为了兼容C++98的代码。
大三律
大三律是说:如果你声明了复制构造函数、赋值运算符,或析构函数中的任意一个,你就得同时声明所有这三个。
为什么?其实很简单,之所以显示声明,就是因为默认的函数不适用,很可能说明其中包含了资源管理,例如new、delete。而这样的操作一定要在这三个函数中都进行处理才正确。
事实上,两种移动操作也是同理。综合起来,在C++11中就应该是“大五律”了。
在C++11标准中,已经将上图绿色的箭头变成deprecate了,也就是说,在将来的C++版本中,可能任意声明这5个成员函数中的一个,就不会自动生成其他几个成员函数了。但当前版本为了兼容C++98代码,还暂时保留图中的绿色箭头。
使用建议
之前有提到过,析构函数最好声明为virtual的。但结合今天的例子,这又会导致无法自动生成两个移动成员函数。
即使一个类现在没有显式声明析构函数,如果将来有一天想在析构函数中加个日志,声明了它,这就会导致原本自动生成的移动构造函数被删除,移动操作都变成了复制操作,可能会大幅影响性能,仅仅是因为声明了一个析构函数。
所以,比较稳妥的办法是,如果这个类可能会复制或移动,那就把两个复制、两个移动,以及析构函数全都声明。如果没有特别的需求,可以直接使用默认实现 =default
网友评论