Item 40:审慎使用 multiple inheritance

对齐 Effective C++ 第三版 Item 40:解决多重继承的名字歧义,推导 diamond 与 virtual inheritance 的布局和初始化成本,并用 public interface base 加 private implementation base 表达正当双角色。

学习目标

  • 能复现两个 base 声明同名函数时的 ambiguity,并用限定调用、using 或 derived wrapper 明确选择
  • 能绘制普通 diamond 与 virtual inheritance 的 subobject 布局,推导 most-derived 初始化责任和成本
  • 能设计一个 public interface base 加 private implementation base 的多继承类型,并判断 composition 是否更清晰
Multiple inheritance decision maplookup → identity → contract edges → measured layoutambiguitytwo base scopesqualify / wrapperdiamondduplicated rootone identity?virtual inheritanceshared virtual basemost-derived initinterface classpublicprivate implpublic private inheritance edges need separate contractspublic is-aprivate implementationcomposition alternativeABI / layout testmultiple inheritance judiciously = every edge, state identity and construction cost is explicit
多继承不是一个整体开关:先消歧,再判断共同身份,最后分别证明 public interface 与 private implementation 的契约。

多继承审查实验

先预测:这条继承边增加了什么?

先预测名字解析、root identity 和 public/private contract,再切换场景查看审查证据。

观察

两个 base scope 都有 checkOut 时,名字查找先收集两个候选,再检查 access;private 候选也不会自动消失。

决策

在 derived wrapper 中写限定调用或 selective using;对客户暴露 borrow 这类意图 API,而不是让每个客户了解继承图。

当前场景 · ambiguity / qualified wrapper

在 derived wrapper 中写限定调用或 selective using;对客户暴露 borrow 这类意图 API,而不是让每个客户了解继承图。

从一个无法决定的 checkOut 开始

class BorrowableItem {
public:
    void checkOut();
};
 
class ElectronicGadget {
private:
    bool checkOut() const;
};
 
class MP3Player : public BorrowableItem, public ElectronicGadget {};
MP3Player player;
// player.checkOut(); // ambiguous

即使 ElectronicGadget::checkOut 是 private,调用仍歧义。C++ 名字查找先找到两个 base scope 中的候选,再判断访问性;编译器不会因为一个候选不可访问就替程序选择另一个。

Item 40 的原则是 Use multiple inheritance judiciously(审慎使用多重继承)。

先预测:把 Gadget 的 checkOut 改成 protected 或 public,是否能消除 ambiguity?答案是否改变,为什么?

用限定名表达选择,而不是依赖猜测

player.BorrowableItem::checkOut();

限定调用最直接,但会让客户知道继承结构。若业务语义是“借出播放器”,更好的 API 是在 derived 中提供意图 wrapper。

class MP3Player : public BorrowableItem, public ElectronicGadget {
public:
    void borrow() { BorrowableItem::checkOut(); }
};

using BorrowableItem::checkOut 也能把一个名字引入 derived scope,但应确认是否真的想公开原始 base contract。wrapper 更适合改名、校验参数和维护 derived invariant。

多重继承首先是多个独立身份

若一个类型 public 继承两个 interface classes,它向客户同时承诺两个 is-a 关系。

class Printable {
public:
    virtual ~Printable() = default;
    virtual void print(std::ostream&) const = 0;
};
 
class Serializable {
public:
    virtual ~Serializable() = default;
    virtual Bytes serialize() const = 0;
};
 
class Invoice final : public Printable, public Serializable {
    // both contracts implemented
};

这种多继承可能合理:Printable 和 Serializable 是正交 capabilities,通常没有 data,也不共享共同 stateful root。客户分别经不同 interface 使用 Invoice。

但若两个 bases 都拥有生命周期状态、copy 规则和同名操作,derived 要同时维护的交叉不变量会迅速增多。

diamond:共同祖先出现两次

class File { /* path and metadata */ };
class InputFile : public File {};
class OutputFile : public File {};
class IOFile : public InputFile, public OutputFile {};

IOFile 含两个 File subobjects:一个经 InputFile 路径,一个经 OutputFile 路径。IOFile 转成 File& 会歧义,path 也有两份。

IOFile file;
File& inputSide = static_cast<InputFile&>(file);
File& outputSide = static_cast<OutputFile&>(file);

两份 File state 可能各自持有不同 path,这到底是需求还是模型错误必须明确回答。

virtual inheritance 共享共同 base

class InputFile : virtual public File {};
class OutputFile : virtual public File {};
class IOFile : public InputFile, public OutputFile {
public:
    explicit IOFile(Path path) : File(std::move(path)) {}
};

virtual 在这里描述 base subobject 共享,与 virtual function 是不同机制。InputFile 和 OutputFile 都声明 virtual inheritance 后,IOFile 中只有一个 File。

most-derived type 负责构造 virtual base

virtual base 由最终完整对象直接初始化,而不是由中间 bases 最终决定。

class File {
public:
    explicit File(Path path);
};
 
class IOFile : public InputFile, public OutputFile {
public:
    explicit IOFile(Path path)
        : File(path), InputFile(), OutputFile() {}
};

当未来再派生 EncryptedIOFile,它才是 most-derived,并负责 File 初始化。这个责任穿过中间层,增加构造知识和演进耦合。

virtual inheritance 的成本模型

共享 base identity 不是免费抽象。实现通常需要额外指针或 offset metadata 来定位 virtual base;访问可能多一次间接计算;对象更大;初始化规则更复杂;cast 和 debugger layout 也更难读。

Item 40 的审慎建议是:非必要不要使用 virtual bases;若必须使用,尽量避免在 virtual base 放 data。纯接口或极小稳定状态更容易管理。

可辩护形态:public interface + private implementation

Item 40 给出一种现实可用的组合:一个 public base 表达公开接口身份,另一个 private base 提供实现和 virtual hooks。

class IPerson {
public:
    virtual ~IPerson() = default;
    virtual std::string name() const = 0;
};
 
class PersonInfo {
public:
    explicit PersonInfo(DatabaseId id) : id_(id) {}
 
    std::string theName() const {
        return nameDelimOpen() + readName(id_) + nameDelimClose();
    }
 
private:
    virtual std::string nameDelimOpen() const { return "["; }
    virtual std::string nameDelimClose() const { return "]"; }
    DatabaseId id_;
};
 
class CPerson final : public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseId id) : PersonInfo(id) {}
    std::string name() const override { return theName(); }
 
private:
    std::string nameDelimOpen() const override { return {}; }
    std::string nameDelimClose() const override { return {}; }
};

CPerson public 继承 IPerson,因为 CPerson is-an IPerson;private 继承 PersonInfo,因为 CPerson 根据 PersonInfo 实现,并需要重定义其 delimiters。两条继承边分别有不同、可说明的语义。

为什么不直接 composition PersonInfo

若 CPerson 只调用 PersonInfo::theName,composition 更好;但示例还需要 override PersonInfo 的 virtual delimiters。普通 member 不能 override 其所属类型的 virtual functions。

可以像 Item 39 那样设计 nested adapter 或修改 PersonInfo 接受 strategy/callback。若 PersonInfo 是不可修改的既有实现,private inheritance 可能是局部成本最低的办法。

class CPerson final : public IPerson {
private:
    class InfoAdapter final : public PersonInfo {
        // overrides and calls owner if needed
    };
 
    InfoAdapter info_;
};

adapter composition 能让 CPerson 的继承图更简单,但增加一个辅助类型与 back-reference。应比较实际依赖,不要机械套规则。

审查多重继承的六张表

  1. 身份表:每条 public edge 是否真是 is-a,能否独立做 substitutability test。
  2. 名字表:base public/protected/private names 是否冲突,客户如何消歧。
  3. 状态表:共同祖先应有一份还是多份 state,谁维护交叉 invariant。
  4. 布局表:是否出现 virtual base、额外 offset、size/alignment 与 ABI 影响。
  5. 构造表:谁初始化 virtual base,constructor failure 如何展开。
  6. 演进表:任一 base 新增 overload、data 或 virtual 是否破坏 derived。

先预测后编译:分别给两个 bases 新增同名 nonvirtual function,观察现有 derived call sites 是否从唯一解析变为 ambiguity。多继承的风险不仅在今天,也在 base 独立演进后。

小结

  • multiple inheritance 同时引入多个 base scopes、身份、状态和构造协议
  • 名字查找的 ambiguity 先于 access checking,private 候选也会参与冲突
  • 普通 diamond 包含多份共同 base;virtual inheritance 可共享一份但有布局与初始化成本
  • most-derived type 负责直接初始化 virtual bases
  • 多个正交纯接口可以合理 public 继承,但每个 contract 都要独立验证
  • public interface base 加 private implementation base 是可辩护形态,前提是每条边语义明确

资料与写作方式声明

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

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

名词解释

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

multiple inheritance

同时直接继承多个 bases 的关系。

ambiguity

名字可沿多条路径找到而无法唯一决定。

lookup-before-access

先查名再检查访问性的顺序。

qualified base call

显式写出目标 base scope 的调用。

disambiguating wrapper

derived 固定 base 选择的意图接口。

selective using declaration

引入选定 base overload set 的声明。

multiple public contracts

同时保持多个公开 base 合约的责任。

orthogonal interface capability

可独立验证且状态不冲突的接口能力。

diamond inheritance

共同 root 分叉后在 derived 汇合的继承图。

duplicated base subobject

普通菱形沿路径产生的多份共同 base。

virtual inheritance

多路径共享共同 base subobject 的机制。

shared virtual base identity

菱形中唯一共同 base 的身份。

most-derived constructor

负责 virtual base 初始化的完整对象构造器。

virtual-base initialization coupling

最终类型依赖远端 virtual base 构造要求。

virtual-base offset metadata

定位 virtual base 的 ABI 布局信息。

virtual inheritance cost

虚继承的空间、访问、构造与诊断代价。

interface class

只定义客户契约的公开抽象 base。

private implementation base

私有复用算法和扩展协议的 base。

public-private inheritance pairing

公开接口与私有实现两种继承角色组合。

legacy extension constraint

既有实现只能通过继承扩展的限制。

multiple-inheritance audit matrix

身份到演进的六维审查表。

练习

  1. 问题 1:multiple inheritance judiciously 与 ambiguity。 客户意图是图书馆借出,Gadget 的同名函数是内部状态检查。
  1. 问题 2:virtual inheritance 与 diamond 的 IOFile。 File 保存唯一 canonical path,InputFile 与 OutputFile 都需访问它。
  1. 问题 3:interface class 与 public private inheritance 的 CPerson。 IPerson 是客户接口,PersonInfo 是不可修改且依靠 virtual hooks 的实现。

讨论

评论区加载中…