定义虚析构函数后,是否需显式声明C++特殊成员函数?
更新Visual Studio编译器后,定义如下简单类时出现警告C5267:“Value的隐式赋值运算符定义已弃用,因它拥有用户提供的析构函数”。
class Value { public: virtual ~Value() = default; virtual void print() = 0; }; class IntValue : public Value { int val; public: IntValue(int val_) : val(val_) { } virtual void print() override; }; // class StrValue : public Value { ... } // 典型用法(2023/12/12补充) std::vector<std::unique_ptr<Value>> values; values.emplace_back(std::make_unique<IntValue>(15)); IntValue a, b; a = b; // 触发警告 IntValue c(a); // 触发警告
根据文档说明:当类存在用户声明的析构函数时,隐式复制赋值运算符的生成会被弃用,隐式复制构造函数同理。针对此情况,提出以下三个问题:
- 若不想使用已被弃用的特性,是否需要显式添加复制构造函数和复制赋值运算符?
- 移动构造函数和移动赋值运算符该如何处理?
- 为确保通过基类引用正确析构,我通常会添加虚析构函数,这是否意味着这类情况通常都需要添加如下所有函数?
class Value { public: virtual ~Value() = default; Value(Value const &) = default; // 因析构函数存在,隐式声明已被弃用 Value(Value &&) = default; // 如果Value有“大对象成员”,是否需要添加以实现优化的移动? Value &operator=(Value const &) = default; // 因析构函数存在,隐式声明已被弃用 Value &operator=(Value &&) = default; // 同上——是否需要添加以实现优化的移动赋值? Value() = default; // 显式复制构造函数会禁用隐式默认构造函数,因此需要显式声明 virtual void print() = 0; };
我觉得这就像多米诺效应——仅仅添加一个常用的虚析构函数,却要编写大量冗余代码。
解答
问题1:是否需要显式添加复制构造和复制赋值运算符?
是的。一旦声明了析构函数(哪怕是=default的虚析构),C++标准会弃用隐式生成的复制构造函数和复制赋值运算符。如果代码中需要使用复制语义(比如示例里的a = b、IntValue c(a)),就必须显式用=default或者自定义实现来声明它们,否则编译器会报弃用警告,未来版本可能直接停止生成这些函数。
需要注意:如果基类是抽象类(像Value这样带纯虚函数),通常不会直接复制基类对象,但派生类的隐式复制函数会因为基类有用户声明的析构函数而受影响,所以如果派生类需要复制语义,同样要显式声明=default或者自定义实现。
问题2:移动构造和移动赋值运算符的处理
移动构造函数和移动赋值运算符的隐式生成不受用户声明的析构函数影响——只要类没有声明复制构造、复制赋值、移动构造、移动赋值中的任何一个,编译器会自动生成默认的移动函数(当类的所有成员都可移动时)。
但如果显式声明了复制构造/赋值(比如用=default声明复制构造),隐式移动函数会被删除。这时候如果需要移动语义,就需要显式用=default声明移动构造和移动赋值。
回到你的场景:如果Value或其派生类有“大对象成员”(比如std::string、std::vector),需要利用移动操作的性能优势,那么显式声明=default的移动函数是有必要的;如果类成员都是简单类型(比如int),默认的移动和复制性能差异不大,也可以不用显式声明,除非明确需要移动语义。
问题3:添加虚析构后是否必须写全所有函数?
不一定,这取决于实际需要的语义:
- 如果你不需要复制语义:可以显式删除复制构造和复制赋值(
=delete),这样既避免警告,又能防止意外的复制操作(对于抽象基类来说,禁止复制往往是合理的,因为通常不会复制多态对象)。 - 如果需要复制语义:才需要显式声明
=default的复制构造和赋值,同时根据是否需要移动语义来决定是否添加移动函数。 - 默认构造函数:只有当显式声明了复制构造/移动构造后,隐式默认构造才会被删除。如果类不需要默认构造(比如
IntValue必须用int参数构造),甚至可以不用声明;如果需要默认构造,再显式=default即可。
举个更简洁的例子,针对抽象基类Value,如果不需要复制语义,可以这样写:
class Value { public: virtual ~Value() = default; // 显式禁止复制,避免警告同时防止意外复制 Value(const Value&) = delete; Value& operator=(const Value&) = delete; // 如果需要移动语义,再添加: // Value(Value&&) = default; // Value& operator=(Value&&) = default; virtual void print() = 0; };
这样既解决了警告问题,又符合多态基类的设计习惯,代码也不会冗余。
内容的提问来源于stack exchange,提问作者Jarek C

