为何声明析构函数未删除隐式生成的移动构造函数?
1. 自定义析构函数后std::move仍能运行的原因
你看到的代码能运行,不是因为编译器生成了移动构造函数,而是调用了隐式生成的拷贝构造函数。
按照C++标准规则:当类存在用户声明的析构函数、拷贝构造函数或拷贝赋值运算符时,编译器不会隐式生成移动构造和移动赋值运算符(它们会被隐式删除)。但当你尝试使用移动构造(比如Test t2 = std::move(t1))时,因为移动构造不可用,编译器会尝试匹配拷贝构造函数——而拷贝构造函数的参数是const Test&,可以绑定右值(std::move(t1)的结果是右值引用),所以最终会调用拷贝构造完成对象初始化。
你的Test类没有用户声明的拷贝构造,所以编译器会为它生成默认的拷贝构造,这就是代码能运行的原因。如果想要禁止这种 fallback 行为,可以显式删除移动构造和拷贝构造:
class Test { public: ~Test() {} Test(const Test&) = delete; Test(Test&&) = delete; protected: int a = 5; };
此时再尝试std::move就会编译报错。
2. 基类声明虚析构函数时无需重定义所有拷贝/移动函数的原因
基类声明虚析构函数,只会影响基类自身的隐式函数生成规则——基类的移动构造/赋值会被隐式删除,但基类的拷贝构造/赋值仍然会被隐式生成(除非你显式删除)。
对于派生类来说,只要派生类没有自己声明的析构、拷贝/移动函数,编译器会为派生类生成默认的拷贝/移动函数,这些默认函数会自动调用基类对应的拷贝/移动函数。只要基类的拷贝/移动函数是可用的(比如基类的拷贝构造是默认生成的),派生类的默认版本就能正常工作。
只有当基类的拷贝/移动函数被显式删除时,派生类的默认拷贝/移动才会出问题——但仅仅是基类有虚析构,并不会导致基类的拷贝构造被删除,所以派生类无需额外重定义就能正常使用。
3. 显式默认拷贝/移动函数时建议定义所有相关函数的原因
这是C++五法则的核心思想:当你需要自定义或显式默认其中一个生命周期管理函数(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)时,通常意味着你的类涉及资源管理(比如动态内存、文件句柄等)。此时如果只处理其中部分函数,可能会导致语义不一致。
举个例子:如果你的类有自定义析构函数(用于释放动态内存),但显式默认了移动构造函数,却没有处理移动赋值运算符,那么移动赋值时可能不会正确转移资源,导致重复释放或资源泄漏。显式要求开发者处理所有相关函数,是为了强制开发者明确对象在拷贝、移动、销毁时的行为,避免因隐式规则带来的意外错误。
这种设计的初衷是让对象的生命周期管理保持一致性,确保资源被正确管理,减少潜在的bug。
内容的提问来源于stack exchange,提问作者Getter

