Item 9:绝不在构造和析构期间调用 virtual 函数

对齐 Effective C++ 第三版 Item 9:解释动态类型在构造时逐层扩展、析构时逐层收缩,识别直接和间接 virtual 调用,并以参数传递或完整构造后启动替代。

学习目标

  • 能解释构造时动态类型逐层扩展、析构时逐层收缩,并预测每个阶段 virtual dispatch 的实际目标
  • 能分析并识别 constructor/destructor 中经 helper、NVI、callback 或 this 泄露发生的间接 virtual 调用
  • 能设计派生数据构造参数或 factory post-construction start,验证完整对象不变量与失败回滚
Lifecycle dispatch / safe start未构造与已销毁的层级不能承载最派生行为virtual functions duringconstruction or destructionphase-limitedno derived statebase constructorBase dispatch onlymembers not readydynamic typeexpands / contractsconstruct / destroypost-construction startfactory owns candidatepublish after successindirect virtual callhelper / NVI / callbackthis escape提前发布风险fully constructedvirtual safe windowrollbackfailure not published移除危险调用只是第一步;factory 还必须保证状态、回滚和 callback publication 顺序
生命周期阶段决定可见的动态类型;需要最派生行为时,把调用推迟到完整构造并由 factory 原子发布。

生命周期 dispatch 实验

先预测:这个 virtual call 现在安全吗?

先预测当前层级、成员 ready 状态和发布时机,再切换场景查看证据。

观察

base constructor 调 virtual 时,派生成员尚未构造;析构退回基类时,派生成员已经销毁。两侧都不是完整对象状态。

决策

移除构造/析构期间的 virtual 依赖;把必要数据作为普通参数传入 Base,或将行为推迟到完整构造之后。

当前场景 · virtual functions during construction or destruction / base constructor

移除构造/析构期间的 virtual 依赖;把必要数据作为普通参数传入 Base,或将行为推迟到完整构造之后。

从“派生对象尚未成为派生对象”开始

class Transaction {
public:
    explicit Transaction(const LogInfo& info)
    {
        logTransaction(info); // virtual
    }
    virtual void logTransaction(const LogInfo&) const = 0;
};
 
class BuyTransaction : public Transaction {
public:
    BuyTransaction() : Transaction{createLogInfo()} {}
    void logTransaction(const LogInfo&) const override;
};

执行 Transaction constructor 时,BuyTransaction 自己的成员和不变量还没有建立。若 virtual dispatch 进入 BuyTransaction override,它可能读取尚未开始生命周期的数据。

在 base constructor 中被视为 Base,在 Derived constructor 完成相应层后才成为完整 Derived。

构造/析构期间受到生命周期阶段限制。 Never call virtual functions during construction or destruction(绝不在构造和析构期间调用 virtual 函数)同时覆盖直接调用和隐藏在 helper、NVI、callback 内的间接路径。

为什么语言要限制 dispatch

Base subobject 必须先构造,Derived members 随后才初始化。反向析构时 Derived members 先消失,Base destructor 最后运行。

construct: virtual bases -> direct bases -> members -> derived body
destroy:   derived body -> members -> direct bases -> virtual bases

只有此时 virtual 调用才能安全依赖最派生状态。限制 dispatch 是保护未构造/已销毁 subobject,不是功能缺陷。

Base constructor 中 virtual 只到 Base

class Base {
public:
    Base() { announce(); }
    virtual ~Base() = default;
private:
    virtual void announce() const { std::puts("Base"); }
};
 
class Derived : public Base {
private:
    void announce() const override { std::puts("Derived"); }
};

构造 Derived 时,Base constructor 调用 Base::announce,不会调用 Derived::announce。

如果 Base 版本是 pure virtual 且没有可调用定义,直接或间接调用可能产生 pure virtual call、链接错误或未定义行为。

编译器对直接调用有时能警告,但间接路径更隐蔽。

间接调用同样危险

class Transaction {
public:
    Transaction() { initialize(); }
private:
    void initialize()
    {
        validateBaseState();
        logTransaction(); // virtual,仍处于 Base constructor
    }
    virtual void logTransaction() const = 0;
};

代码审查若只搜索 constructor 体中的 virtualName() 会漏掉 helper、lambda、callback 和注册表调用。

注册 observer、启动线程、把 this 放进全局容器都可能造成更严重的提前调用。

NVI 不会绕开生命周期规则

在正常生命周期中可集中不变量检查;但 Base constructor 调 public NVI,内部 virtual hook 仍只到 Base 层。

void Base::initializeInterface()
{
    checkBasePrecondition();
    doInitialize(); // virtual,构造期间仍受限
}

模式不能改变对象生命周期。不要用额外非虚包装掩盖时机错误。

析构阶段动态能力反向收缩

class Base {
public:
    virtual ~Base() { audit(); }
private:
    virtual void audit() const { /* Base state only */ }
};

Base destructor 中 audit 不会进入 Derived override,因为 Derived destructor 和成员已经结束。

每一层 destructor 应清理该层自己拥有的资源;不要让 Base 要求 Derived 回调帮它收尾。

替代一:派生先计算数据再传给 Base

若 Base constructor 只是想记录派生类型相关信息,让 Derived 以 static helper 计算参数:

class Transaction {
protected:
    explicit Transaction(LogInfo info)
        : info_{std::move(info)}
    {
        logTransaction(info_); // non-virtual Base implementation
    }
private:
    void logTransaction(const LogInfo&) const;
    LogInfo info_;
};
 
class BuyTransaction : public Transaction {
public:
    BuyTransaction()
        : Transaction{createLogInfo()} {}
private:
    static LogInfo createLogInfo();
};

static helper 没有 this,不能误读未初始化成员。Base 获得完整值后使用非虚逻辑。

适合参数可在构造前确定的场景。

替代二:完整构造后显式 start

某些初始化确实需要最派生行为,例如注册插件能力或调用虚拟设备。用 factory 保证构造后再启动:

class Component {
public:
    virtual ~Component() = default;
    void start()
    {
        if (state_ != State::Constructed)
            throw std::logic_error{"invalid start state"};
        doStart();
        state_ = State::Started;
    }
private:
    virtual void doStart() = 0;
    State state_{State::Constructed};
};
 
template<class T, class... Args>
std::unique_ptr<T> makeStarted(Args&&... args)
{
    auto candidate = std::make_unique<T>(std::forward<Args>(args)...);
    candidate->start();
    return candidate;
}

start 失败时 candidate unique_ptr 自动销毁完整对象,不发布半启动实例。

防止调用方忘记 start,应隐藏 public constructor 或只暴露 factory。

两阶段初始化的状态成本

post-construction 不是免费方案:所有 public 操作必须拒绝未 Started 状态;重复 start、并发 start、start 失败重试要有协议。

enum class State { Constructed, Starting, Started, Failed, Stopped };

优先让 start 在局部构建候选资源,成功后一次提交;外部注册尽量最后发生。析构按当前 state 只清理已拥有部分且不抛异常。

callback 注册必须晚于完整构造

constructor 中注册会发生 this escape。即使事件源承诺“稍后”,并发线程也可能立即回调。

void Sensor::doStart()
{
    auto token = eventLoop_.subscribe([this](Event e) {
        onEvent(e); // 此时对象已完整构造且 Started 提交需有顺序
    });
    subscription_ = std::move(token);
}

并发系统还需同步;单纯“移出 constructor”不自动解决 data race。

审计直接与隐藏路径

编译器警告如 -Wvirtual-call/静态分析可辅助,但需要人工追踪非虚 helper 和 this escape。

先预测:Base constructor 调 helper,helper 调 pure virtual;Derived override 依赖 member flag。写出每层对象生命周期、实际 dispatch 与可能诊断,再运行日志 probe。

  • Base/Derived 每层 constructor/destructor 记录当前成员 ready 标志。
  • 任何 virtual override 断言其层级 ready,构造/析构测试不得触发。
  • factory start 前所有成员已初始化,start 失败对象未发布。
  • callback 只在 Started 后可见,stop 后先注销再销毁成员。
  • direct、helper、lambda 与 NVI 调用链均在静态审计清单中。
  • sanitizer 和线程测试覆盖 this escape 与 start/stop 竞争。

小结

  • 构造时 dynamic type 逐层扩展,析构时逐层收缩
  • Base constructor/destructor 中 virtual 调用不会进入尚未构造或已销毁的 Derived
  • helper、NVI、callback 与 this escape 都可能隐藏生命周期内 virtual 调用
  • 可提前计算的派生信息作为普通构造参数传给 Base
  • 必须使用最派生行为时,由 factory 在完整构造后执行 start,并失败不发布
  • 两阶段初始化需要状态机、rollback 与 callback publication 协议

资料与写作方式声明

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

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

名词解释

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

construction-time dynamic type

构造析构当前阶段可参与 virtual dispatch 的最派生层。

virtual dispatch

按当前动态类型选择 override 的调用过程。

subobject lifetime

基类或成员子对象从构造到析构的独立生命周期。

fully constructed state

所有层成员和不变量都完成后的运行阶段。

phase-limited dispatch

构造析构期间只分派到当前层级的规则。

pure virtual call

生命周期阶段触及无有效实现纯虚函数的错误。

indirect virtual call

helper 内隐藏的 virtual 调用路径。

this escape

构造完成前把 this 发布给外部代码。

NVI idiom

non-virtual public 接口调用 virtual hook 的模式。

virtual hook

派生 override、供基类算法定制步骤的函数。

destruction-time type contraction

析构时动态能力逐层退回基类的过程。

teardown dependency inversion

基类析构错误依赖已销毁派生行为。

derived construction data

派生在进入 Base 构造前计算并传入的值。

non-virtual base initialization

只依赖基类与参数的普通初始化逻辑。

post-construction start

完整构造后才执行 virtual 启动的阶段。

publish-after-start

构造启动全成功后才交给外部 owner 的规则。

initialization state machine

管理 Constructed、Started、Failed 等状态的模型。

startup rollback

启动部分成功后失败时撤销副作用的责任。

callback registration

向事件源发布对象回调或 this 的操作。

callback publication ordering

回调可见前状态与订阅都已提交的顺序。

lifecycle virtual-call audit

追踪构造析构调用链中 virtual 的审计。

练习

  1. 问题 1:virtual functions during construction or destruction 与 base constructor。 Base constructor/helper/destructor 都调 virtual,Derived override 读取成员,写出每次实际目标与风险。
  1. 问题 2:dynamic type 与 base constructor。 基类构造想记录派生交易类型,设计不使用两阶段对象的修复。
  1. 问题 3:dynamic type 与 post-construction start。 插件必须完整构造后 virtual 注册 callback,设计 factory、状态与失败回滚。

讨论

评论区加载中…