Item 39:审慎使用 private inheritance

对齐 Effective C++ 第三版 Item 39:解释 private inheritance 的 implemented-in-terms-of 语义,比较 Timer/Widget 的继承与组合设计,并在 protected/virtual 需求或 empty base optimization 场景中审慎决策。

学习目标

  • 能解释 private inheritance 阻止客户隐式转换、却允许 derived 访问 protected 与重定义 virtual 的规则
  • 能比较 Timer/Widget 的 private inheritance 与 nested adapter composition,并设计更窄的耦合边界
  • 能推导 empty base optimization 的布局收益,判断何时采用 EBO 或 [[no_unique_address]]
Private inheritance decision mapimplemented-in-terms-of → capability need → measured costimplemented-in-terms-ofTimer protocol / protected客户不可向上转换compositionmember / nested adapter先隔离继承角色private inheritance judiciouslyprotected / virtual / EBO先测量再承担耦合empty base optimization is evidence, not a reflexmember sizeofempty base optimizationno_unique_addressABI / alignment空间收益必须超过 protected、virtual、callback 和 layout coupling 的维护成本
private inheritance 是实现工具,不是默认关系;先看 composition 与 nested adapter,再用真实布局测量证明 EBO 值得采用。

实现继承决策实验

先预测:这份继承耦合值得吗?

先预测客户可见性、virtual/protected 需求和布局收益,再切换方案查看证据。

关系

Widget 私有继承 Timer 后,外部不能隐式转 Timer*;Widget 自身仍能访问 protected 并参与 Timer 的 virtual protocol。

决策

只有需要 protected access 或直接 override virtual 且 nested adapter 不合适时才保留 private inheritance;否则不要承受继承耦合。

当前方案 · private inheritance judiciously / Timer

只有需要 protected access 或直接 override virtual 且 nested adapter 不合适时才保留 private inheritance;否则不要承受继承耦合。

从 public 转换失败开始

class Timer {
public:
    explicit Timer(std::chrono::milliseconds interval);
    void start();
 
protected:
    virtual void onTick();
};
 
class Widget : private Timer {
public:
    Widget() : Timer(std::chrono::seconds(1)) {}
};

Widget 的成员和 friends 能使用 Timer 的 public/protected 部分,但普通客户不能把 Widget* 隐式转换为 Timer*。因此这段声明没有对外承诺 Widget is-a Timer。

Item 39 的原则是 Use private inheritance judiciously(审慎使用 private 继承)。

private inheritance 表达 implemented-in-terms-of,而不是 public inheritance 的 is-a。

先预测:void inspect(Timer&) 能否直接接收普通客户传入的 Widget?Widget member function 内部把 this 转成 Timer pointer 又是否可行?两处访问上下文不同。

access control 改变的是客户可见关系

void inspect(Timer& timer);
 
Widget widget;
// inspect(widget); // error: Timer is an inaccessible base

在 Widget 的 member/friend 上下文中,转换仍可使用;外部上下文则不可访问。Timer 的 public start 也不会自动成为 Widget 的 public API,除非 Widget 主动包装或用 declaration 暴露。

class Widget : private Timer {
public:
    void beginUpdates() { start(); }
};

客户依赖的是 beginUpdates,Timer 只是实现细节。这一点与 composition 相同,但耦合强度不同。

为什么通常仍应优先 composition

class Widget {
public:
    Widget() : timer_(std::chrono::seconds(1)) {}
    void beginUpdates() { timer_.start(); }
 
private:
    Timer timer_;
};

composition 更容易看懂:Widget has-a Timer implementation object。Widget 只依赖 Timer public contract,Timer protected 细节变化不会直接影响 Widget;也可保存多个 Timer,并独立命名每个角色。

composition 无法直接完成的两类需求

private inheritance 有两个重要能力:访问 protected members,以及重定义 virtual functions。普通成员对象做不到这两点。

class Widget : private Timer {
private:
    void onTick() override {
        ++ticks_;
        repaint();
    }
 
    int ticks_ = 0;
};

虽然 Timer 是 private base,Timer 内部的 virtual call 仍会分派到 Widget::onTick。继承访问级别不关闭多态;它只限制谁能把 Widget 当 Timer 使用。

这正是 private inheritance 的合理候选:Widget 确实需要成为 Timer 实现协议中的 override participant,但不希望客户看到 Timer 身份。

用 nested adapter 把继承角色收窄

即使需要 override,也可以让一个私有辅助类继承 Timer,而让 Widget 自身保持 composition。

class Widget {
private:
    class WidgetTimer final : public Timer {
    public:
        WidgetTimer(Widget& owner, std::chrono::milliseconds interval)
            : Timer(interval), owner_(owner) {}
 
    private:
        void onTick() override { owner_.handleTick(); }
        Widget& owner_;
    };
 
    void handleTick() {
        ++ticks_;
        repaint();
    }
 
    WidgetTimer timer_{*this, std::chrono::seconds(1)};
    int ticks_ = 0;
};

这种设计代码稍多,却有三个收益:Widget 本身不依赖 Timer protected layout;WidgetTimer 可以 final;Widget 可同时拥有多个不同 interval 的 adapters。代价是 adapter 需要安全回指 owner。

若 Timer 可能在 Widget construction 未完成时或 destruction 已开始后回调,必须推迟 start、先停止 timer,并处理并发 callback join。

private inheritance 的第二个理由:EBO

C++ 完整对象即使没有数据成员,通常也必须占至少一个 byte,以便同类型相邻对象有不同地址。把 empty type 作为 member,padding 可能使 owner 增大。

struct EmptyPolicy {};
 
struct MemberStorage {
    EmptyPolicy policy;
    int value;
};
 
struct BaseStorage : private EmptyPolicy {
    int value;
};

常见 ABI 下,sizeof(BaseStorage) 可等于 sizeof(int),而 MemberStorage 可能更大。这就是 EBO,也是 Item 39 指出的少数 private inheritance 空间理由。

EBO 适合 allocator、deleter、comparison policy 等空策略;标准库实现常用 compressed pair 一类结构保存 empty policy 与非空对象。

EBO 不是免费的普遍保证

对象布局受类型关系、相同类型子对象、alignment、virtual bases 和 ABI 影响。不要只凭直觉断言大小。

static_assert(std::is_empty_v<EmptyPolicy>);
static_assert(sizeof(BaseStorage) <= sizeof(MemberStorage));

若内存收益只有几个 bytes,而 private inheritance 让接口和诊断变复杂,审慎结论可能仍是 composition。

C++20 的成员语义替代

struct ModernStorage {
    [[no_unique_address]] EmptyPolicy policy;
    int value;
};

它让代码继续表达 has-a/member relationship,同时请求类似 EBO 的空间优化。仍应检查 compiler、ABI、相同类型成员和 portability 要求;该属性不是改变生命周期或访问规则的魔法。

一套审慎决策顺序

面对“为了实现而复用 Base”的需求,按以下顺序判断:

  1. 若 Derived 必须可替换 Base,使用 public inheritance,而不是 private。
  2. 若只调用 Base public operations,优先 composition。
  3. 若需要 override Base virtual hook,尝试 nested adapter,把继承角色隔离。
  4. 若必须直接访问 protected 且 adapter 不合适,private inheritance 才成为候选。
  5. 若理由是 EBO,先比较 member、[[no_unique_address]] 与 private base 的实际布局。
  6. 检查 construction/destruction callback、copy/move、owner back-reference 与多线程停止协议。

先预测一个重构的结果:把 Widget private Timer 改成 WidgetTimer member 后,哪些测试应保持不变,哪些 lifetime tests 必须新增?

小结

  • private inheritance 表达 implemented-in-terms-of,不向普通客户提供 Derived-to-Base 转换
  • derived 仍可使用 base public/protected implementation,并可 override virtual hooks
  • 仅复用 public operations 时,composition 通常耦合更小、角色更清晰
  • nested adapter 可把必须的继承隔离到最小辅助类型
  • empty base optimization 是少数空间理由,但必须测量布局与维护成本
  • C++20 [[no_unique_address]] 常能在保留成员语义时获得类似优化

资料与写作方式声明

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

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

名词解释

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

private inheritance

对客户隐藏 base 身份的实现继承。

implemented-in-terms-of

借用实现但不承诺可替换。

inaccessible base conversion

外部不可用的 derived-to-base 转换。

implementation-side base access

成员与 friends 对 private base 的实现访问。

member-based implementation reuse

通过成员 public contract 复用能力。

inheritance implementation coupling

对 protected、virtual 和 base layout 的紧耦合。

protected implementation access

base 为 derived 提供的非公开扩展访问。

private virtual override

private derived 参与 base virtual dispatch。

nested inheritance adapter

私有辅助类型继承并向 owner 翻译事件。

inheritance role isolation

把继承限制在最小实现角色。

adapter back-reference lifetime

owner 覆盖 adapter 回调期的寿命约束。

empty class

无实例状态和 virtual 机制的类型。

empty base optimization

空 base 不增加 owner 大小的布局优化。

empty policy object

仅由类型和函数表达行为的空策略。

compressed pair pattern

组合空策略与载荷并压缩布局的结构。

layout measurement

对大小、对齐和目标 ABI 的实测。

no_unique_address member

允许空成员重叠存储的 C++20 属性用法。

judicious inheritance decision

逐层验证 private inheritance 必要性的流程。

练习

  1. 问题 1:private inheritance judiciously 与 Timer。 Widget 需要重定义 onTick,但不应对外成为 Timer。
  1. 问题 2:empty base optimization 与布局测量。 EmptyPolicy 与 int payload 分别用普通成员、private base 和 no_unique_address member 保存。
  1. 问题 3:implemented-in-terms-of、composition 与 callback 生命周期。 Timer 在后台线程回调,Widget 可移动。

讨论

评论区加载中…