Item 7:为多态基类声明 virtual destructor
对齐 Effective C++ 第三版 Item 7:分析经 base pointer 删除派生对象的完整析构链,选择 public virtual、protected non-virtual 或 pure virtual destructor,并验证所有权与 ABI。
学习目标
- 能解释
delete Base*对实际 Derived 对象为何需要 virtual destructor,并追踪最派生到基类的完整销毁顺序 - 能比较 public virtual、protected non-virtual、pure virtual destructor、custom deleter 与 destroy function 的所有权语义
- 能设计计数、sanitizer、factory 和 ABI 测试,验证多态销毁完整且非多态值类型没有无意引入 virtual 布局
多态销毁实验
先预测:delete Base* 会走到哪里?
先预测静态/动态类型和 allocator 归属,再切换场景查看析构链证据。
观察
只要对象可能经 base pointer 删除,destructors virtual in polymorphic base classes 就是所有权契约,而不是日志风格。
决策
public virtual destructor 让 delete Base* 按动态类型进入最派生析构,再逆序清理成员和基类。
当前场景 · destructors virtual in polymorphic base classes / virtual destructor
public virtual destructor 让 delete Base* 按动态类型进入最派生析构,再逆序清理成员和基类。
从“删除成功为何仍可能未定义”开始
设工厂隐藏实现类型:
class TimeKeeper {
public:
virtual std::chrono::system_clock::time_point now() const = 0;
};
class AtomicClock : public TimeKeeper {
public:
~AtomicClock();
std::chrono::system_clock::time_point now() const override;
private:
std::unique_ptr<NativeClock> clock_;
};
std::unique_ptr<TimeKeeper> make_clock();如果 TimeKeeper destructor 非 virtual,unique_ptr<TimeKeeper> 的默认 deleter 最终执行 delete TimeKeeper*,而实际对象是 AtomicClock。
需要
↡通过基类指针删除时按动态类型进入最派生析构链的 virtual 析构函数。。
否则行为未定义,不只是“可能少打一条日志”。
delete-expression 同时依赖静态与动态类型
↡源码中指针表达式声明的类型,例如 delete Base* 中的 Base。 ↡对象运行时实际构造的最派生类型,例如 Base* 指向的 Derived。非 virtual destructor 只让 delete-expression 看见静态 Base 析构;动态 Derived 的资源与成员无法获得可靠完整销毁。
class TimeKeeper {
public:
virtual ~TimeKeeper() = default;
virtual TimePoint now() const = 0;
};完整链路先执行 AtomicClock::~AtomicClock,释放 clock_,再执行 TimeKeeper::~TimeKeeper,最后释放存储。
何时必须声明 virtual destructor
↡设计上允许由调用方持有 Base* 所有权,并经该接口销毁实际派生对象。满足时基类 destructor 应 public virtual。一个实用判断:类含任何 virtual function,且对象可能经 base pointer 销毁,就必须审查 destructor。 Declare destructors virtual in polymorphic base classes(为多态基类声明 virtual destructor)描述的正是这一所有权条件;若基类明确禁止外部 base-pointer deletion,则应以 protected non-virtual destructor 表达另一种契约。
class Plugin {
public:
virtual ~Plugin() = default;
virtual void start() = 0;
virtual void stop() noexcept = 0;
};
std::unique_ptr<Plugin> load_plugin(const Config&);Factory 返回 unique_ptr<Base> 本身就是强烈证据:default_delete 需要正确 base deletion。
shared_ptr 从 shared_ptr<Derived> 构造时可能在 control block 保存 Derived deleter,但不能把这种细节当基类设计借口;其他 Base* 删除仍危险。
public virtual 与 protected non-virtual
如果基类用于多态调用但不允许通过 Base* 删除,可让 destructor protected non-virtual:
class StackOnlyInterface {
public:
virtual void run() = 0;
protected:
~StackOnlyInterface() = default;
};这把不合法 delete 拒绝在编译期。代价是不能直接使用 unique_ptr<Base> default deleter,所有权必须由具体类型或 custom deleter 管理。
可用于 C ABI 或模块边界,但接口复杂度更高。
pure virtual destructor 仍需定义
↡声明为 `= 0`、使类成为 abstract,同时仍会在派生析构链中被调用的析构函数。class AbstractSession {
public:
virtual ~AbstractSession() = 0;
};
AbstractSession::~AbstractSession() = default;即使 pure virtual,派生对象析构仍必须执行基类 destructor,因此需要定义;缺失会在链接阶段失败。若类已有其他 pure virtual,析构无需再设 pure 才能抽象。
不要继承没有多态契约的标准值类型
std::string、std::vector 等没有 virtual destructor,因为它们不是设计给 public polymorphic inheritance 的基类。
↡复制移动表示独立值,通常不用于运行期替换和 base-pointer deletion 的类型。class TaggedString : public std::string { /* 危险 public 继承 */ };
std::string* text = new TaggedString;
delete text; // undefined behavior应组合 string 成员,或定义自己的多态接口。
不是所有 destructor 都应 virtual
↡明确禁止进一步派生的类,编译器可据此优化且不存在派生销毁链。值类型或 final class 没有 base-pointer deletion 场景,virtual destructor 只会改变类型性质。
class Point final {
public:
Point(double x, double y) : x_{x}, y_{y} {}
private:
double x_{};
double y_{};
};首个 virtual 常增加对象大小、对齐和 ABI 约束;已有其他 virtual 的多态类通常已付出 vptr 成本,析构不 virtual 却保留致命漏洞。
ABI 与插件边界
↡已编译模块之间对对象布局、vtable、allocator 和调用约定的二进制契约。给已发布 class 新增首个 virtual 会改变布局和 ABI。若跨 DLL/共享库分配与释放,常用配对 factory/destroy function:
extern "C" Plugin* create_plugin(const HostApi* host);
extern "C" void destroy_plugin(Plugin* plugin) noexcept;调用方用 custom deleter 包装:
using PluginPtr = std::unique_ptr<Plugin, void (*)(Plugin*)>;
PluginPtr plugin{create_plugin(&host), &destroy_plugin};这不是 virtual destructor 的通用替代,而是明确跨模块所有权与 allocator。模块内部仍可使用 virtual destructor 保证层次正确。
析构访问与所有权必须一致
↡析构函数是 public、protected 或 private,从而决定哪些调用方能结束对象生命周期。public virtual 表示“任何 owner 可经 Base 销毁”;protected non-virtual 表示“外部不能经 Base 销毁”;private destructor 常配 factory/release 或 intrusive lifetime。
↡接口明确说明谁拥有对象、通过何种静态类型和释放函数结束生命周期的规则。不能一边返回 owning Base*,一边让 destructor protected;类型和工厂签名必须一致。
测试动态析构链
↡记录每一层析构是否执行、执行顺序和资源释放次数的测试。struct Probe {
std::vector<std::string>& log;
std::string name;
~Probe() { log.push_back(name); }
};在 Derived/Base 各放 Probe,经 unique_ptr<Base> 销毁,断言派生成员、Derived body、基类成员与 Base body 的预期顺序。AddressSanitizer/LeakSanitizer 捕获泄漏和错误 deallocation。
先预测:给一个已有 virtual method 但 non-virtual destructor 的接口,通过 factory 创建派生再释放,会执行哪些析构?改为 virtual 后重新写出链路并验证。
std::has_virtual_destructor_v<Base>与设计预期一致。unique_ptr<Base>销毁派生时派生资源计数回零。- protected non-virtual base 的
delete Base*是 compile-fail test。 - pure virtual destructor 有定义且派生销毁可链接。
- final/value 类型大小和 trivial 性没有被无意 virtual 改变。
- 插件 create/destroy 在同一模块 allocator 下配对。
小结
- 经 Base* 删除实际 Derived 时,polymorphic base 必须有 virtual destructor
- virtual 分派进入最派生析构,再按成员与基类逆序完成完整销毁
- public virtual 支持 base deletion;protected non-virtual 在编译期禁止外部 base delete
- pure virtual destructor 可使类抽象,但仍必须提供定义
- 没有多态用途的 value/final 类型不应无故添加 virtual
- ABI 边界可用配对 destroy function/custom deleter,但仍要明确唯一销毁协议
名词解释
本章出现的专业名词,用大白话再讲一遍。
- polymorphic hierarchy
通过 virtual 接口按动态类型选择实现的体系。
- virtual destructor
经基类指针删除时进入最派生析构链的析构函数。
- static type
- 指针表达式在源码中声明的类型。
- dynamic type
对象运行时实际构造的最派生类型。
- dynamic destruction chain
从最派生到根基类的完整析构序列。
- polymorphic deletion contract
允许经 Base* 所有者销毁派生对象的契约。
- polymorphic owner
以基类智能指针拥有实际派生对象的所有者。
- protected non-virtual destructor
禁止外部经 Base* delete 的析构策略。
- custom deleter
智能指针保存并在销毁时执行的释放函数对象。
- pure virtual destructor
声明为纯虚但仍需定义并执行的析构函数。
- abstract base class
不可直接实例化、用于规定派生接口的类。
- value type
具有独立值语义且不用于运行期基类替换的类型。
- non-polymorphic base misuse
继承未设计多态销毁的值类型作为 public 基类。
- final class
- 明确禁止继续派生的类。
- vptr
多态对象中通常指向虚函数表的隐藏指针。
- binary interface
模块间对象布局、vtable、allocator 与调用约定。
- destroy function
由创建模块提供的配对对象释放接口。
- destructor accessibility
析构访问级别对可销毁调用方的限制。
- destruction ownership protocol
规定 owner、静态类型和释放动作的协议。
- destruction-chain test
验证每层析构顺序和资源释放次数的测试。
练习
- 问题 1:destructors virtual in polymorphic base classes。 为 factory 返回的 Plugin、栈上策略接口和 final Point 分别设计 destructor。
- 问题 2:virtual destructor 与 delete through base pointer。 设计一个不依赖“程序没崩”的多态删除测试。
- 问题 3:virtual destructor 与 destroy function。 对象由动态库创建、主程序持有,说明两者如何取舍。