条款17:理解特殊成员函数的生成

📅 2026/7/28 10:31:25
条款17:理解特殊成员函数的生成
目录条款17理解特殊成员函数的生成Understand special member function generation生成机制分析生成机制总结最佳实践1多态基类最佳实践2条款17理解特殊成员函数的生成Understandspecial member functiongeneration生成机制分析特殊/特种成员函数指那些C会自行生成的成员函数。C98原有默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符这些函数仅在需要时才会被生成即代码中使用了它们而类中并未显式声明仅当类没有声明任何构造函数时才会生成默认构造函数这些函数都是public和inline的这些函数都是非虚的仅当基类的析构函数为虚的派生类的析构函数才是虚的C11新增移动构造函数、移动赋值运算符注意事项移动函数的“生成规则”和“行为表现”类似于拷贝函数。生成规则仅在需要时才生成行为表现对类的“非静态成员”执行“按成员移动”操作移动构造函数将按照其形参rhs的各个非静态成员对于本类的对应成员执行移动构造同时还会移动构造它的基类部分若存在。移动赋值运算符将按照其形参rhs的各个非静态成员对于本类的对应成员执行移动赋值同时还会移动赋值它的基类部分若存在。注意事项“按成员移动”实际上更像是按成员的移动请求不支持移动的类型将通过其拷贝操作进行“移动”。分析1两个拷贝函数是彼此独立声明了其中一个并不会阻止编译器生成另一个。C98和C11都成立两个移动函数并不彼此独立声明了其中一个就会阻止编译器生成另一个。分析2一旦声明了拷贝函数构造或赋值编译器就不会生成移动函数。一旦声明了移动函数构造或赋值编译器就会废除删除拷贝函数。分析3大三律Rule of Three如果声明了拷贝构造函数、拷贝赋值运算符或析构函数中的任何一个就应该同时声明所有三个。具体思想为如果有改写拷贝函数的需求往往意味着该类需要进行资源管理如堆上内存、互斥量等。进一步地在一种拷贝函数中进行的任何资源管理也极有可能在另一种拷贝函数中也需要进行析构函数也会参与到资源管理中通常为释放之。推论由此可知理论上一旦声明了析构函数则编译器生成的拷贝函数就不适用于该类也就不应该被自动生成。然而实际上在C98中用户声明的析构函数不会阻止编译器生成拷贝函数当时论证过程没有得到充分重视。在C11中这种行为仍然保持若对拷贝函数的生成条件施加更严格的限制就会破坏太多的遗留代码。一旦声明了析构函数编译器仍然会生成拷贝函数废弃行为。一旦声明了析构函数编译器就不会生成移动函数。之所以移动操作的生成条件比拷贝操作的更严格是因为移动操作加入的比较晚不太会牵扯到以前的代码而拷贝操作在以前代码中大量使用现在修改规则就不太方便了。生成机制总结结论1“默认构造函数”的生成条件与C98的机制相同该类未显式声明任何的构造函数结论2“析构函数”的生成条件与C98的机制基本相同该类中未显式声明析构函数相同点仅当基类的析构函数为虚的派生类的析构函数才是虚的相同点析构函数默认为noexcept不同点、唯一区别结论3“拷贝函数”的生成条件拷贝构造函数仅当类中未显式声明拷贝构造函数时拷贝构造函数才生成。当类中显式声明了移动函数时拷贝构造函数将被删除。当类中显式声明了拷贝赋值运算符或析构函数时拷贝构造函数仍然生成已经成为废弃行为。拷贝赋值运算符仅当类中未显式声明拷贝赋值运算符时拷贝赋值运算符才生成。当类中显式声明了移动函数时拷贝赋值运算符将被删除。当类中显式声明了拷贝构造函数或析构函数时拷贝赋值运算符仍然生成已经成为废弃行为。结论4“移动函数”的生成条件如果需要时仅当以下三者同时成立该类未显式声明任何拷贝函数“分析2”该类未显式声明任何移动函数“分析1”该类未显式声明任何析构函数“分析3”结论5在任何情况下“成员函数模板”都不会阻止特殊成员函数的生成。即使这些模板的具现结果生成了拷贝构造函数或拷贝赋值运算符的签名当TWidget时编译器始终生成Widget的拷贝函数和移动函数。待补充条款26最佳实践1多态基类多态基类通常会有虚析构函数否则某些操作比如通过基类指针或者引用对派生类对象执行delete或者typeid等会产生未定义或误导性结果。然而除非类继承而来的析构函数本身就是虚的否则只能通过“将析构函数声明为虚的”来达到“拥有虚析构函数”的目的。通常情况下虚析构函数的默认实现是正确的因此使用“default”是提供默认实现的不错方式。然而一旦用户声明了析构函数编译器就会阻止移动函数的生成。如果该类需要支持移动性就需要为移动函数加上“default”给予编译器以生成移动函数的机会。进一步地一旦用户声明了移动函数编译器就会阻止拷贝函数的生成。如果该类也需要支持拷贝性就需要再为拷贝函数加上“default”。最佳实践2即使编译器能够为类生成拷贝函数和移动函数并且这些生成函数也符合需要即默认行为如你所愿你也应该遵从以下最佳实践方式自动声明这些函数并以“default”作为它们的定义。优点是意图更清晰避免微妙的错误缺点是多费些功夫。案例分析考虑一个用于表示“字符串表格StringTable”的类即允许通过整型ID来快速检索字符串值的数据结构。实现1可行不推荐程序设计该类未显式声明拷贝函数、移动函数和析构函数编译器将在这些函数有需要调用时自动生成它们假设行为符合预期。需求变更在默认构造函数和析构函数中添加打印日志信息的功能具体实现如下非常简单。问题分析上述改动看起来合情合理但是声明析构函数有潜在的副作用它阻止了移动函数的生成。当然拷贝函数的生成不受影响。因此代码能通过编译运行也能通过功能测试即打日志的功能。针对移动操作的测试也能够通过因为即使该类不支持移动操作针对它进行移动操作的请求也能通过编译和运行。而这些请求触发的是拷贝操作。这意味着StringTable对象的移动实际上执行的是StringTable对象的拷贝即底层std::mapint, std::string对象的拷贝。然而std::mapint, std::string对象的拷贝操作很可能比移动操作慢几个数量级。可见简单的动作向类中加入一个析构函数就可能引发可观的性能问题实现2可行推荐在类中显式声明拷贝函数和移动函数并以“ default”来提供定义。