目录
条款17:理解特殊成员函数的生成(Understand special member function generation)
生成机制(分析)
生成机制(总结)
最佳实践1(多态基类)
最佳实践2
条款17:理解特殊成员函数的生成(Understand special member function generation)
生成机制(分析)
特殊/特种成员函数:指那些C++会自行生成的成员函数。
C++98(原有):默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符
- 这些函数仅在需要时才会被生成(即代码中使用了它们,而类中并未显式声明)
- 仅当类没有声明任何构造函数时,才会生成默认构造函数
- 这些函数都是public和inline的
- 这些函数都是非虚的(仅当基类的析构函数为虚的,派生类的析构函数才是虚的)
C++11(新增):移动构造函数、移动赋值运算符
- 注意事项:移动函数的“生成规则”和“行为表现”类似于拷贝函数。
- 生成规则:仅在需要时才生成
- 行为表现:对类的“非静态成员”执行“按成员移动”操作
- 移动构造函数:将按照其形参rhs的各个非静态成员对于本类的对应成员执行移动构造;同时还会移动构造它的基类部分(若存在)。
- 移动赋值运算符:将按照其形参rhs的各个非静态成员对于本类的对应成员执行移动赋值;同时还会移动赋值它的基类部分(若存在)。
- 注意事项:“按成员移动”实际上更像是按成员的移动请求,不支持移动的类型将通过其拷贝操作进行“移动”。

分析1:
- 两个拷贝函数是彼此独立:声明了其中一个,并不会阻止编译器生成另一个。(C++98和C++11都成立)
- 两个移动函数并不彼此独立:声明了其中一个,就会阻止编译器生成另一个。
分析2:
- 一旦声明了拷贝函数(构造或赋值),编译器就不会生成移动函数。
- 一旦声明了移动函数(构造或赋值),编译器就会废除(删除)拷贝函数。
分析3:
- 大三律(Rule of Three):如果声明了拷贝构造函数、拷贝赋值运算符或析构函数中的任何一个,就应该同时声明所有三个。具体思想为:如果有改写拷贝函数的需求,往往意味着该类需要进行资源管理(如堆上内存、互斥量等)。进一步地,在一种拷贝函数中进行的任何资源管理,也极有可能在另一种拷贝函数中也需要进行;析构函数也会参与到资源管理中(通常为释放之)。
- 推论:由此可知(理论上),一旦声明了析构函数,则编译器生成的拷贝函数就不适用于该类,也就不应该被自动生成。然而(实际上),在C++98中,用户声明的析构函数不会阻止编译器生成拷贝函数(当时论证过程没有得到充分重视)。在C++11中,这种行为仍然保持(若对拷贝函数的生成条件施加更严格的限制,就会破坏太多的遗留代码)。
- 一旦声明了析构函数,编译器仍然会生成拷贝函数(废弃行为)。
- 一旦声明了析构函数,编译器就不会生成移动函数。
之所以移动操作的生成条件比拷贝操作的更严格,是因为移动操作加入的比较晚,不太会牵扯到以前的代码;而拷贝操作在以前代码中大量使用,现在修改规则就不太方便了。
生成机制(总结)

结论1:“默认构造函数”的生成条件(与C++98的机制相同)
- 该类未显式声明任何的构造函数
结论2:“析构函数”的生成条件(与C++98的机制基本相同)
- 该类中未显式声明析构函数(相同点)
- 仅当基类的析构函数为虚的,派生类的析构函数才是虚的(相同点)
- 析构函数默认为noexcept(不同点、唯一区别)
结论3:“拷贝函数”的生成条件
- 拷贝构造函数:
- 仅当类中未显式声明拷贝构造函数时,拷贝构造函数才生成。
- 当类中显式声明了移动函数时,拷贝构造函数将被删除。
- 当类中显式声明了拷贝赋值运算符或析构函数时,拷贝构造函数仍然生成已经成为废弃行为。
- 拷贝赋值运算符:
- 仅当类中未显式声明拷贝赋值运算符时,拷贝赋值运算符才生成。
- 当类中显式声明了移动函数时,拷贝赋值运算符将被删除。
- 当类中显式声明了拷贝构造函数或析构函数时,拷贝赋值运算符仍然生成已经成为废弃行为。
结论4:“移动函数”的生成条件(如果需要时)仅当以下三者同时成立
- 该类未显式声明任何拷贝函数(“分析2”)
- 该类未显式声明任何移动函数(“分析1”)
- 该类未显式声明任何析构函数(“分析3”)
结论5:在任何情况下,“成员函数模板”都不会阻止特殊成员函数的生成。
- 即使这些模板的具现结果生成了拷贝构造函数或拷贝赋值运算符的签名(当T=Widget时),编译器始终生成Widget的拷贝函数和移动函数。
- 待补充(条款26)

最佳实践1(多态基类)
多态基类通常会有虚析构函数,否则某些操作(比如通过基类指针或者引用对派生类对象执行delete或者typeid等)会产生未定义或误导性结果。然而,除非类继承而来的析构函数本身就是虚的,否则只能通过“将析构函数声明为虚的”来达到“拥有虚析构函数”的目的。通常情况下,虚析构函数的默认实现是正确的,因此使用“=default”是提供默认实现的不错方式。
然而,一旦用户声明了析构函数,编译器就会阻止移动函数的生成。如果该类需要支持移动性,就需要为移动函数加上“=default”,给予编译器以生成移动函数的机会。
进一步地,一旦用户声明了移动函数,编译器就会阻止拷贝函数的生成。如果该类也需要支持拷贝性,就需要再为拷贝函数加上“=default”。

最佳实践2
即使编译器能够为类生成拷贝函数和移动函数,并且这些生成函数也符合需要(即默认行为如你所愿),你也应该遵从以下最佳实践方式:自动声明这些函数,并以“=default”作为它们的定义。(优点是意图更清晰,避免微妙的错误;缺点是多费些功夫。)
案例分析:考虑一个用于表示“字符串表格(StringTable)”的类,即允许通过整型ID来快速检索字符串值的数据结构。
实现1(可行,不推荐):
- 程序设计:该类未显式声明拷贝函数、移动函数和析构函数,编译器将在这些函数有需要调用时自动生成它们(假设行为符合预期)。

- 需求变更:在默认构造函数和析构函数中,添加打印日志信息的功能,具体实现如下(非常简单)。

- 问题分析:上述改动看起来合情合理,但是声明析构函数有潜在的副作用:它阻止了移动函数的生成。当然,拷贝函数的生成不受影响。因此代码能通过编译,运行,也能通过功能测试(即打日志的功能)。针对移动操作的测试也能够通过,因为即使该类不支持移动操作,针对它进行移动操作的请求也能通过编译和运行。而这些请求触发的是拷贝操作。这意味着StringTable对象的移动,实际上执行的是StringTable对象的拷贝,即底层std::map<int, std::string>对象的拷贝。然而,std::map<int, std::string>对象的拷贝操作,很可能比移动操作慢几个数量级。可见,简单的动作(向类中加入一个析构函数)就可能引发可观的性能问题!
实现2(可行,推荐):在类中显式声明拷贝函数和移动函数,并以“= default”来提供定义。
网硕互联帮助中心





评论前必须登录!
注册