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 布局
Polymorphic deletion / destruction chain静态接口 → 动态析构 → allocator 与 ABI 归属destructors virtual inpolymorphic base classesownership contractBase* can ownvirtual destructordynamic dispatchcomplete cleanupdelete through base pointerstatic + dynamic typeDerived → Basedestroy functionmodule-owned freecustom deleterdynamic destruction chainDerived → members → Basepublic virtualBase owner allowedprotected non-virtualBase delete blockedABI evidenceallocator identityvirtual destructor 解决层次,destroy function 解决模块;两条 ownership 契约都必须可测试
virtual destructor 与 destroy function 关注不同边界:前者保证析构链,后者保证创建模块与释放模块配对。

多态销毁实验

先预测: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。

需要

否则行为未定义,不只是“可能少打一条日志”。

delete-expression 同时依赖静态与动态类型

非 virtual destructor 只让 delete-expression 看见静态 Base 析构;动态 Derived 的资源与成员无法获得可靠完整销毁。

class TimeKeeper {
public:
    virtual ~TimeKeeper() = default;
    virtual TimePoint now() const = 0;
};

完整链路先执行 AtomicClock::~AtomicClock,释放 clock_,再执行 TimeKeeper::~TimeKeeper,最后释放存储。

何时必须声明 virtual destructor

满足时基类 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 仍需定义

class AbstractSession {
public:
    virtual ~AbstractSession() = 0;
};
 
AbstractSession::~AbstractSession() = default;

即使 pure virtual,派生对象析构仍必须执行基类 destructor,因此需要定义;缺失会在链接阶段失败。若类已有其他 pure virtual,析构无需再设 pure 才能抽象。

不要继承没有多态契约的标准值类型

std::string、std::vector 等没有 virtual destructor,因为它们不是设计给 public polymorphic inheritance 的基类。

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 与插件边界

给已发布 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 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,但仍要明确唯一销毁协议

资料与写作方式声明

本章以Effective C++, Third Edition, Item 7权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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. 问题 1:destructors virtual in polymorphic base classes。 为 factory 返回的 Plugin、栈上策略接口和 final Point 分别设计 destructor。
  1. 问题 2:virtual destructor 与 delete through base pointer。 设计一个不依赖“程序没崩”的多态删除测试。
  1. 问题 3:virtual destructor 与 destroy function。 对象由动态库创建、主程序持有,说明两者如何取舍。

讨论

评论区加载中…