Item 32:确定 public 继承塑造出 is-a 关系
对齐 Effective C++ 第三版 Item 32:把 public inheritance 视为严格 substitutability 契约,以 Square–Rectangle 和 Penguin–Bird 反例检查前置、后置、不变量和错误模型,并用能力接口与组合重构。
学习目标
- 能解释 public inheritance models is-a 与 Liskov substitution 的可观察契约
- 能复现 Square–Rectangle 在 setWidth 后置条件上的冲突,并判断前置、后置与 invariant 变化
- 能设计 capability interface、composition 或独立 value types,替代只为代码复用建立的错误继承
替换契约实验
先预测:这个 derived 能替换 base 吗?
先预测每个场景的合法客户、状态不变量和失败语义,再切换场景查看测试证据。
base contract
Rectangle::setWidth 只改变 width,height 保持原值;这是合法 base client 可以依赖的后置条件。
判断
Square 为维持 width == height 必须同时改变 height,因此不是可替换的 Rectangle。
当前场景 · square rectangle / invariant
Square 为维持 width == height 必须同时改变 height,因此不是可替换的 Rectangle。
从“正方形当然是长方形”开始质疑
在数学分类中,Square 是 Rectangle;在 C++ 类型系统中,public inheritance 会让所有 Square*/Square& 自动可用于需要 Rectangle*/Rectangle& 的地方。
Item 32 的原则是 Make sure public inheritance models is-a(确定 public 继承塑模 is-a)。这里 is-a 不是日常语言的“看起来属于”,而是程序行为的严格承诺。
↡客户程序只依赖 base contract 时,将对象替换为 derived 后仍保持正确。如果 Square 不能满足 Rectangle 的全部可观察操作,它就不应 public inherit Rectangle,即使两者共享 width/height 数据。
public 继承向客户承诺什么
base 的每个 public operation 都成为 derived 的契约。derived 可以实现得更快、返回更具体类型或加强结果,但不能让原本合法的 base 客户失效。
↡base type 对合法输入、结果、状态、错误和复杂度给客户的可依赖说明。Liskov substitution(里氏替换)可具体化为:
- derived 不应加强 precondition。
- derived 不应削弱 postcondition。
- derived 应保持 base invariant。
- derived 不应引入 base 客户无法处理的新失败语义。
- derived 的 lifetime/ownership 行为不能让 base 用法悬空或泄漏。
这些名称不是为了形式主义,而是给 code review 可检查的清单。
Rectangle 客户的合法假设
定义一个可独立修改宽高的 Rectangle:
class Rectangle {
public:
virtual ~Rectangle() = default;
virtual void setWidth(int value);
virtual void setHeight(int value);
int width() const noexcept;
int height() const noexcept;
};setWidth 的自然后置条件是 width 变为 value,height 保持原值。
void makeWider(Rectangle& value) {
const int oldHeight = value.height();
value.setWidth(value.width() + 10);
assert(value.height() == oldHeight);
}这个函数对 Rectangle contract 是正确的,不属于“客户不该这样写”。
↡只使用 base public API,并依赖其公开前置/后置条件的程序。Square 无法同时满足两个契约
Square invariant 要求 width 始终等于 height:
class Square : public Rectangle {
public:
void setWidth(int value) override {
Rectangle::setWidth(value);
Rectangle::setHeight(value);
}
void setHeight(int value) override {
Rectangle::setWidth(value);
Rectangle::setHeight(value);
}
};为了保持自身 invariant,Square::setWidth 必须改变 height;这削弱了 Rectangle::setWidth 的“height unchanged”后置条件。传给 makeWider 后 assertion 失败。
↡derived 为维护自身规则而改变 base operation 已承诺效果的替换冲突。“抛异常表示不支持”也不满足 is-a
Square 可以让 setWidth 抛 UnsupportedOperation,但 Rectangle 客户原本可合法调用;derived 增强了 precondition 或引入新失败。
void Square::setWidth(int) {
throw UnsupportedOperation{};
}这不恢复 substitutability,只把静默违约改为运行期违约。
↡所有继承到 derived public interface 的 operations 对其合法状态都必须有一致语义。若 operation 对某 subtype 没有意义,base abstraction 过宽或继承关系错误。
现实分类不等于软件行为模型
Penguin 在生物分类上是 Bird,但如果 Bird API 声明 fly(),Penguin 无法替换 Flying Bird。
错误设计:
class Bird {
public:
virtual void fly() = 0;
};
class Penguin : public Bird {
public:
void fly() override; // throw/no-op 都违背 contract
};软件模型应服务客户操作,而不是复制百科分类树。
拆分可选能力
若只有部分 birds 会飞,fly 不应属于所有 Bird:
class Bird {
public:
virtual ~Bird() = default;
virtual void eat() = 0;
};
class Flyable {
public:
virtual ~Flyable() = default;
virtual void fly() = 0;
};
class Sparrow : public Bird, public Flyable { /* ... */ };
class Penguin : public Bird { /* ... */ };调用 fly 的客户依赖 Flyable,而非 Bird;类型系统拒绝 Penguin。
↡客户端只依赖完成任务所需最小操作集合的设计。这样错误从 runtime throw 提前到 compile-time mismatch。
Square 与 Rectangle 可共享更小抽象
可以建只读 Shape/Bounds 接口,或让二者成为独立 value types:
class RectBounds {
public:
int width() const noexcept;
int height() const noexcept;
};
class Rectangle {
RectBounds bounds_;
};
class Square {
int side_;
};需要统一绘制时,两者可实现只读 Drawable/Shape,不暴露独立 setWidth/setHeight。
↡只包含所有 implementations 都能一致满足的最小公共行为。共享代码应通过 helper/composition,不必伪造 public is-a。
代码复用不是 public 继承的理由
“Derived 想复用 Base implementation”只说明实现关系,不说明客户可替换。
↡为了获得已有字段或函数实现而建立关系,但没有行为替换语义。优先 composition;private inheritance 适用于少数需要 protected access/empty-base optimization 的实现关系,Item 39 会深入。
↡类型拥有另一个对象并通过其 public API 复用能力的 has-a 结构。public inheritance 必须从 base client contract 出发证明,而不是从 derived 写代码方便出发。
前置条件不能变严格
若 base send(Message) 接受任意合法 Message,derived 只接受 size < 1KB,就拒绝了 base 客户可传的合法值。
可让 base contract 本来就声明 limit,或把 restricted sender 作为不同类型/策略,不应隐藏在 override。
↡base 接受的合法 input/state 集合,所有 derived 必须至少同样宽。参数类型相同不证明 semantic domain 相同,测试要覆盖边界值。
后置条件和错误模型不能变弱
base save() 承诺成功后 durable,derived 只写 memory cache 却返回成功,削弱 postcondition。
base no-throw operation 被 override 改成可能抛也会破坏客户;C++ noexcept override rules 能检查部分情况,但业务错误语义仍需测试。
↡derived 在异常、错误码、重试和 side effects 上保持 base 客户可处理的失败契约。性能复杂度若属于 public promise,也不能从 O(1) 悄然变 O(n) 并让实时客户失效。
public inheritance 也包含 lifetime 契约
若 base 有 virtual destructor、copy/clone、ownership 和 thread-safety promise,derived 必须保持。
↡base 指针/引用管理 derived 时,销毁、复制、移动和借用仍安全的要求。例如 base clone 承诺独立 deep copy,derived 不能返回共享 mutable state;base thread-safe const query,derived 不能使用 unsynchronized mutable cache。
↡derived 引入资源或缓存后仍维持 base 声明的并发与 ownership 行为。is-a 不只检查函数名字和返回值。
用 base contract suite 验证每个 derived
为 base 写参数化契约测试,并对每个 concrete derived 运行:
template<class Factory>
void rectangleContract(Factory makeRectangle) {
auto value = makeRectangle();
const int oldHeight = value->height();
value->setWidth(value->width() + 10);
CHECK(value->height() == oldHeight);
}compile checks 只能证明可转换,contract suite 才证明行为。
评审继承关系的证据矩阵
先预测每个 base operation 对候选 derived 的合法输入、结果和 invariant,再验证:
- 列出 base public/virtual/non-virtual operations 与 documented pre/postconditions。
- 对每个 derived 标记是否加强 precondition、削弱 postcondition或新增失败。
- property tests 覆盖 Square/Rectangle width-height 状态空间。
- base contract suite 对每个 factory/derived 重跑。
- ownership tests 覆盖 virtual destruction、clone、copy/move 和 borrowed handles。
- concurrency/complexity tests 覆盖 base 承诺的非功能属性。
- 客户代码不包含 derived type checks 或 “except subtype X” 分支。
- 只复用 implementation 的候选改成 composition 后比较 API 清晰度。
任何一行需要“derived 例外”都应重新审查 public inheritance。
小结
- public inheritance models is-a 意味 derived 可替换 base,不只是现实分类或字段相似
- derived 不能加强 precondition、削弱 postcondition、破坏 base invariant 或错误模型
- Square 为保持边长相等必须改变 Rectangle setWidth 语义,因此不能满足该 base contract
- Penguin–Bird 问题应拆分 Flyable capability,让不支持的操作从 base interface 消失
- 只为 implementation reuse 应选择 composition/helper,而不是 public inheritance
- base contract suite 与 inheritance substitution matrix 提供逐 derived 的行为证据
名词解释
本章出现的专业名词,用大白话再讲一遍。
- public inheritance is-a
derived 可在所有 base contract 场景替换 base。
- behavioral substitutability
替换对象后客户程序仍正确。
- base observable contract
base 对输入结果状态和失败的承诺。
- precondition contravariance rule
derived 不能要求更多输入条件。
- postcondition covariance rule
derived 不能少提供结果保证。
- operation postcondition
操作成功后客户可断言的状态。
- valid base client
只依赖 base public contract 的程序。
- square invariant
Square 的 width 必须等于 height。
- invariant-postcondition conflict
derived invariant 与 base 后置条件冲突。
- unsupported inherited operation
derived 对合法 base 操作表示不支持。
- total inherited contract
全部继承 operations 对 subtype 都有语义。
- domain taxonomy
现实属性分类而非程序行为契约。
- behavioral capability set
抽象类型提供的可执行操作集合。
- capability interface
只声明一种可选行为的小接口。
- interface segregation
客户只依赖完成任务的最小操作集。
- composition without substitutability
共享实现但不声明可替换关系。
- common semantic abstraction
所有实现都一致满足的最小行为。
- implementation-reuse motivation
仅为获得已有代码建立关系的动机。
- composition has-a
以成员对象复用另一类型能力。
- strengthened precondition
derived 缩小 base 合法输入集合。
- accepted-domain preservation
derived 至少接受 base 的全部合法输入。
- weakened postcondition
derived 成功结果少于 base 承诺。
- error-model substitutability
derived 保持 base 失败语义。
- lifetime substitutability
derived 保持 base 销毁复制借用安全。
- nonfunctional contract preservation
保持 thread/complexity/ownership 承诺。
- base contract suite
对全部 subtype 重跑的 base 行为测试。
- subtype conformance test
自动验证 subtype 可替换性的测试。
- inheritance substitution matrix
逐操作记录 subtype 契约与证据的表。
练习
- 问题 1:public inheritance models is-a 与 square rectangle。 写出合法 Rectangle 客户并说明 Square override 无法同时满足两个 invariant。
- 问题 2:is-a 与 liskov substitution 的 Bird/Penguin 重构。 Bird 当前要求 fly,Penguin override 抛异常,请设计 capability hierarchy。
- 问题 3:liskov substitution 的契约测试与非功能保证。 Base 查询承诺 thread-safe O(1) 且不抛,Derived 使用无锁 lazy cache,偶尔分配并抛异常,请判断。