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,替代只为代码复用建立的错误继承
Public inheritance contract mappublic inheritance models is-a → client substitutability → evidencepublic inheritance models is-aderived → base公开契约成为承诺is-avalid base client前置 / 后置 / invariantliskov substitutioncontract suite + matrix输入、结果、失败、生命周期square rectangle failure → reshape the abstractionsquare rectangleheight unchanged ↔ width == heightcapability interfacecomposition分类相似不是行为替换证据;每个 base operation 都要有 derived 的可观察契约证据
public inheritance 的箭头终点不是“字段一样”,而是所有合法 base client 都能安全替换;失败时应缩小接口或改用组合。

替换契约实验

先预测:这个 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 不是日常语言的“看起来属于”,而是程序行为的严格承诺。

如果 Square 不能满足 Rectangle 的全部可观察操作,它就不应 public inherit Rectangle,即使两者共享 width/height 数据。

public 继承向客户承诺什么

base 的每个 public operation 都成为 derived 的契约。derived 可以实现得更快、返回更具体类型或加强结果,但不能让原本合法的 base 客户失效。

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 是正确的,不属于“客户不该这样写”。

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 失败。

“抛异常表示不支持”也不满足 is-a

Square 可以让 setWidth 抛 UnsupportedOperation,但 Rectangle 客户原本可合法调用;derived 增强了 precondition 或引入新失败。

void Square::setWidth(int) {
    throw UnsupportedOperation{};
}

这不恢复 substitutability,只把静默违约改为运行期违约。

若 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。

共享代码应通过 helper/composition,不必伪造 public is-a。

代码复用不是 public 继承的理由

“Derived 想复用 Base implementation”只说明实现关系,不说明客户可替换。

优先 composition;private inheritance 适用于少数需要 protected access/empty-base optimization 的实现关系,Item 39 会深入。

public inheritance 必须从 base client contract 出发证明,而不是从 derived 写代码方便出发。

前置条件不能变严格

若 base send(Message) 接受任意合法 Message,derived 只接受 size < 1KB,就拒绝了 base 客户可传的合法值。

可让 base contract 本来就声明 limit,或把 restricted sender 作为不同类型/策略,不应隐藏在 override。

参数类型相同不证明 semantic domain 相同,测试要覆盖边界值。

后置条件和错误模型不能变弱

base save() 承诺成功后 durable,derived 只写 memory cache 却返回成功,削弱 postcondition。

base no-throw operation 被 override 改成可能抛也会破坏客户;C++ noexcept override rules 能检查部分情况,但业务错误语义仍需测试。

性能复杂度若属于 public promise,也不能从 O(1) 悄然变 O(n) 并让实时客户失效。

public inheritance 也包含 lifetime 契约

若 base 有 virtual destructor、copy/clone、ownership 和 thread-safety promise,derived 必须保持。

例如 base clone 承诺独立 deep copy,derived 不能返回共享 mutable state;base thread-safe const query,derived 不能使用 unsynchronized mutable cache。

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 的行为证据

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:public inheritance models is-a 与 square rectangle。 写出合法 Rectangle 客户并说明 Square override 无法同时满足两个 invariant。
  1. 问题 2:is-a 与 liskov substitution 的 Bird/Penguin 重构。 Bird 当前要求 fly,Penguin override 抛异常,请设计 capability hierarchy。
  1. 问题 3:liskov substitution 的契约测试与非功能保证。 Base 查询承诺 thread-safe O(1) 且不抛,Derived 使用无锁 lazy cache,偶尔分配并抛异常,请判断。

讨论

评论区加载中…