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]]
实现继承决策实验
先预测:这份继承耦合值得吗?
先预测客户可见性、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,并独立命名每个角色。
↡derived 直接依赖 base protected 成员、virtual hooks 和对象布局而形成的紧密实现关系。composition 无法直接完成的两类需求
private inheritance 有两个重要能力:访问 protected members,以及重定义 virtual functions。普通成员对象做不到这两点。
↡只有 base、derived 与其 friends 可访问的实现扩展点或状态。 ↡derived 为 base virtual hook 提供运行期行为的覆盖能力。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。
↡adapter 保存 owner 引用时,owner 必须覆盖 adapter callback 全部执行期的约束。若 Timer 可能在 Widget construction 未完成时或 destruction 已开始后回调,必须推迟 start、先停止 timer,并处理并发 callback join。
private inheritance 的第二个理由:EBO
C++ 完整对象即使没有数据成员,通常也必须占至少一个 byte,以便同类型相邻对象有不同地址。把 empty type 作为 member,padding 可能使 owner 增大。
↡没有非静态数据成员、virtual functions 或 virtual bases 的轻量策略类型。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”的需求,按以下顺序判断:
- 若 Derived 必须可替换 Base,使用 public inheritance,而不是 private。
- 若只调用 Base public operations,优先 composition。
- 若需要 override Base virtual hook,尝试 nested adapter,把继承角色隔离。
- 若必须直接访问 protected 且 adapter 不合适,private inheritance 才成为候选。
- 若理由是 EBO,先比较 member、
[[no_unique_address]]与 private base 的实际布局。 - 检查 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]]常能在保留成员语义时获得类似优化
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 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:private inheritance judiciously 与 Timer。 Widget 需要重定义 onTick,但不应对外成为 Timer。
- 问题 2:empty base optimization 与布局测量。 EmptyPolicy 与 int payload 分别用普通成员、private base 和 no_unique_address member 保存。
- 问题 3:implemented-in-terms-of、composition 与 callback 生命周期。 Timer 在后台线程回调,Widget 可移动。