Item 34:区分接口继承和实现继承
对齐 Effective C++ 第三版 Item 34:用 pure virtual、impure virtual 与 non-virtual 分别表达只继承接口、接口加默认实现、接口加强制实现,并避免派生类无意继承错误默认行为。
学习目标
- 能区分 pure virtual、impure virtual、non-virtual 分别传递的接口与实现继承契约
- 能复现 Airplane subtype 无意继承错误 default implementation,并改成显式复用
- 能设计 pure virtual body、protected helper 或 NVI wrapper,在强制选择与代码复用间保持清晰边界
继承政策实验
先预测:遗漏 override 会怎样?
先预测接口责任、默认实现和固定流程的失败方式,再切换政策查看验证证据。
契约
每个 concrete derived 都必须提供 fly 或 draw 的实现;遗漏 override 应在编译期让类型保持 abstract。
政策判断
把必须由 subtype 确认的行为声明为 pure virtual;共享算法放 protected helper,derived 仍显式选择。
当前政策 · pure virtual / interface
把必须由 subtype 确认的行为声明为 pure virtual;共享算法放 protected helper,derived 仍显式选择。
从同一个 virtual 关键字的三种意图开始
继承层次中,derived 总会继承 base 的 public interface;是否继承 implementation,则由函数声明形式决定。
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
virtual void reportError(std::string_view message);
int objectId() const noexcept;
};Item 34 的原则是 Differentiate inheritance of interface and inheritance of implementation(区分接口继承和实现继承)。三个 functions 看似只差 =0/virtual,实际表达三种不同政策。
pure virtual:只要求接口
draw() = 0 表示 Shape 只知道“每个 concrete shape 必须可绘制”,没有对所有形状都合理的默认算法。
class Circle : public Shape {
public:
void draw() const override;
};未实现 draw 的 derived 仍是 abstract,不能实例化。错误在 compile time 暴露。
↡仍含至少一个未实现 pure virtual operation、不能直接构造对象的 class。pure virtual 适合“所有 subtype 都必须做,但 base 不能安全替它们决定如何做”。
impure virtual:接口加默认实现
reportError 是普通 virtual(原书称 impure virtual):base 声明接口并提供可继承 default body。
void Shape::reportError(std::string_view message) {
defaultLogger().write(message);
}只有当默认对每个合法 subtype 都安全时,才应允许“什么也不写”代表接受默认。
non-virtual:接口加强制实现
objectId() 是 non-virtual:所有 Shape 客户都获得接口,所有 derived 都必须使用同一 base implementation。
int Shape::objectId() const noexcept {
return objectId_;
}derived 若声明同名函数只会 hiding/redefinition,无法改变经 Base reference 的静态绑定,Item 36 会深入。
三种声明是设计决策,不是默认语法
↡class author 为每个 operation 明确选择是否强制 override、允许默认或禁止定制。- pure virtual:继承接口,不自动继承实现。
- impure virtual:继承接口和可覆盖默认实现。
- non-virtual:继承接口和强制统一实现。
不要把“未来可能会 override”当成把所有函数设 virtual 的理由,也不要因少写代码给所有 virtual 提供默认。
Airplane 默认飞行的隐藏风险
假设 ModelA 与 ModelB 使用同一飞行算法,于是在 base 提供 default:
class Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
class ModelA : public Airplane {};
class ModelB : public Airplane {};后来 ModelC 使用完全不同协议,但作者忘记 override:
class ModelC : public Airplane {
// 忘记 override fly,仍然可以实例化
};代码编译并调用错误 defaultFly;接口没有迫使 ModelC author 确认默认是否适用。
↡default implementation 对部分 subtype 不合法,却因遗漏 override 被自动继承的错误。pure virtual 加 helper 强迫显式选择
class Airplane {
public:
virtual ~Airplane() = default;
virtual void fly(const Airport& destination) = 0;
protected:
void defaultFly(const Airport& destination);
};ModelA/B 明确选择默认:
void ModelA::fly(const Airport& destination) {
defaultFly(destination);
}
void ModelB::fly(const Airport& destination) {
defaultFly(destination);
}ModelC 必须写 custom implementation,否则保持 abstract。
↡不属于 public polymorphic interface、只供 derived 显式复用的 base implementation。 ↡derived override 代码明确调用共享 helper,表明作者已选择默认算法。重复一行 forwarding 的成本远低于错误飞行协议。
pure virtual 也可以拥有 definition
C++ 允许为 pure virtual function 提供 out-of-class body;它仍是 pure,concrete derived 仍必须 override。
↡标记 `=0` 的 virtual 在 class 外提供可由 qualified call 使用的函数体。class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination) {
defaultFlightProtocol(destination);
}
void ModelA::fly(const Airport& destination) {
Airplane::fly(destination);
}这种写法把 interface 与 optional implementation 放在同一名字下;protected helper 往往更直观,也可限制外部直接调用。
pure virtual destructor 必须有 definition
destructor 即使 pure virtual,派生对象销毁链仍会调用 base destructor,因此必须提供 body。
↡使 class abstract、同时仍需在派生析构链执行定义的纯虚析构函数。class Interface {
public:
virtual ~Interface() = 0;
};
Interface::~Interface() = default;普通 pure virtual 无需 body,除非显式 qualified use;destructor 是结构性例外。
default implementation 必须满足所有 subtype
impure virtual 合理条件是:默认对所有当前与可预见 derived 都正确,只是某些 derived 能提供更佳/特化行为。
↡无论 derived 是否 override,base default 都满足 base contract 和 subtype invariant 的性质。例如日志 hook 的 default no-op 是否安全取决于审计要求;如果所有操作必须记录,no-op 就不是合法默认。
↡default body 只为了方便,却无法证明对所有 subtype 满足 correctness contract 的气味。默认应由 contract 证明,不由“多数实现相同”投票决定。
non-virtual 用于固定算法和不变量
当 base 必须统一执行前置检查、锁、统计或后置验证,可用 non-virtual public function 包装 virtual hook。
↡public non-virtual operation 固定流程,并调用 private/protected virtual customization hook 的模式。class Task {
public:
Result<void, Error> run() {
validate();
auto result = doRun();
record(result);
return result;
}
private:
virtual Result<void, Error> doRun() = 0;
};Item 35 会进一步比较 NVI、strategy 和 function objects;这里强调 non-virtual 表达“implementation 不可替换”。
access 与 virtual 是两个维度
virtual function 不必 public。public non-virtual API + private virtual hook 可分离“谁能调用”和“谁能定制”。
↡public/protected/private 决定调用访问,virtual/non-virtual 决定动态实现选择,两者相互独立。 ↡只能由 base control flow 调用,但允许 derived override 的 private virtual function。derived 可以 override private virtual,即使不能直接调用它;access 检查与 overriding 是不同机制。
default helper 也要有稳定 contract
shared implementation 不是“内部随便写”。ModelA/B 依赖 defaultFly 的前置、后置、异常和性能。
↡derived 显式复用 helper 时可以依赖的输入、结果、失败与副作用承诺。helper 改动可能影响多个 subtypes,需要共享 contract tests;若 subtypes 开始在 helper 前后堆补丁,可能应抽 strategy/composition。
↡多个 derived 对同一 protected body 产生隐式耦合,修改需同步验证的范围。实现复用仍是 dependency,不是免费抽象。
override/final 让政策可检查
所有 intended overrides 使用 override;不允许进一步定制时使用 final。
这不能替代选择 pure/impure/nonvirtual,但能防 signature 漂移和越界定制。
用 subtype 选择矩阵验收声明
先预测新增 subtype 忘记声明 function 时会发生 compile error、继承 default 还是固定 base behavior,再验证:
- pure virtual:每个 concrete factory 都必须提供 override,遗漏 compile fail。
- impure virtual:default 对每个 subtype 运行 base contract suite,证明 universally safe。
- non-virtual:经 base/derived static type 调用结果一致,不允许同名 redefinition。
- Airplane A/B 显式调用 helper,C 使用 custom implementation。
- pure virtual body 只通过 qualified call 复用,virtual dispatch 仍要求 override。
- pure virtual destructor 有 definition 且经 base owner 完整销毁。
- NVI 前后检查在所有 derived hook 成功、失败、异常路径执行。
- override/final compile tests 捕获签名和扩展政策变化。
每个 function declaration 都应能对应一条明确 policy,而不是“先 virtual 再说”。
小结
- pure virtual 表示继承 interface,concrete derived 必须实现
- impure virtual 表示继承 interface 与可选 default implementation,遗漏 override 会静默采用默认
- non-virtual 表示继承 interface 与强制统一 implementation,不应被 derived 重定义
- default 不对所有 subtype 安全时,用 pure virtual 强迫选择,并以 protected helper 显式复用
- pure virtual 可有 definition;derived 仍须 override,pure virtual destructor 则必须有 body
- NVI 以 non-virtual public algorithm 固定流程、virtual hook 定制步骤
名词解释
本章出现的专业名词,用大白话再讲一遍。
- inheritance of interface
derived 获得 base operation 与行为契约。
- inheritance of implementation
derived 直接复用 base function body。
- pure virtual function
要求 concrete derived override 的接口声明。
- abstract derived class
仍有 pure operation、不可实例化的 derived。
- impure virtual function
带可继承默认实现的普通 virtual。
- inherited default implementation
未 override 时自动采用的 base body。
- non-virtual function
静态绑定且实现不可多态替换的操作。
- invariant implementation policy
全部 subtype 使用统一 base 实现的政策。
- inheritance policy decision
选择强制、可选或禁止定制的决策。
- declaration-level intent
函数声明直接表达 derived 责任。
- silent default adoption
derived 无声明即接受默认实现。
- accidental implementation inheritance
例外 subtype 遗漏 override 后继承错误默认。
- protected implementation helper
仅供 derived 显式复用的 base body。
- explicit default reuse
override 明确调用共享 helper。
- pure virtual definition
pure virtual 的可选 out-of-class body。
- qualified pure-body call
以 Base::function 显式调用 pure body。
- pure virtual destructor
仍需定义并参与析构链的纯虚析构。
- required pure-function body
pure operation 仍需实现的结构性情况。
- universally safe default
对全部 subtype 都满足契约的默认实现。
- convenience-only virtual default
只为少写代码而无通用正确性的默认。
- non-virtual interface pattern
public nonvirtual 流程调用 virtual hook。
- fixed public algorithm
所有 subtype 必经的不可覆盖流程。
- access-dispatch orthogonality
访问控制与 virtual dispatch 相互独立。
- private virtual hook
base 控制调用但 derived 可 override 的 hook。
- reusable implementation contract
derived 可依赖的 helper 行为保证。
- inherited implementation coupling
多个 subtype 对共享 base body 的依赖。
- override verification
compiler 检查真实覆盖关系。
- final customization boundary
禁止进一步 override/inheritance 的边界。
- interface-implementation inheritance matrix
接口与实现继承政策验证表。
练习
- 问题 1:inheritance of interface 与 inheritance of implementation 的 Airplane 政策。 ModelA/B 共用 default,ModelC 必须自定义,但当前忘记 override 仍可编译。
- 问题 2:pure virtual、impure virtual、non-virtual 的 Shape 政策。 draw 无通用实现;reportError 有对所有 shape 安全默认;objectId 必须统一,分别选择。
- 问题 3:inheritance of implementation 与 NVI 的固定流程。 Task::run 必须统一验证、审计和错误转换,但执行算法随 subtype 变化。