Chapter 14:Reusing Code in C++

对齐第6版 Chapter 14:比较 containment、private/protected inheritance,掌握 class template 实例化、multiple inheritance、virtual base 与歧义消解。

学习目标

  • 能比较 containment、private inheritance、protected inheritance 与 public inheritance 的关系、接口暴露和耦合成本
  • 能实现固定容量 class template,并复现不同类型实例化对默认构造、复制和移动操作的要求
  • 能分析 multiple inheritance 的名字歧义、重复祖先和 virtual base 初始化责任,设计显式消歧方案

为什么“复用代码”必须先回答对象关系

C++ 提供包含、不同访问方式的继承、模板和多重继承,但语法都不是设计理由。先问领域关系:Owner has-a Component、Derived is-a Base,还是某个类型仅在实现上借用一个策略?关系决定调用者应该看到什么,也决定未来改动耦合到谁。

containment 默认把复用留在实现边界

包含(containment)把一个类对象作为另一个类的成员,表达 has-a。Owner 决定是否以及如何转发成员能力,Component 的 public API 不会自动变成 Owner 的接口。

#include <string>
 
class Report {
public:
    explicit Report(std::string title) : title_{std::move(title)} {}
    std::size_t titleLength() const { return title_.size(); }
 
private:
    std::string title_;
};

Report has-a string,但不是 string:接受 std::string& 的函数不应接收 Report。成员按声明顺序构造并逆序析构,外层复制控制默认组合成员的复制语义;这使 RAII 成员能够自然形成 Rule of Zero。

private inheritance 复用基类实现但不承诺 is-a

私有继承(private inheritance)把 Base 的 public/protected member 变为 Derived 的 private 实现;外部不能把 Derived 隐式转成 Base。它表达 implemented-in-terms-of,适用于确实需要覆盖 virtual hook、访问 protected 或利用空基类优化的少数情况。

class Timer {
protected:
    virtual void onTick() = 0;
public:
    void tick() { onTick(); }
};
 
class Session : private Timer {
public:
    void advance() { tick(); }
private:
    void onTick() override { ++elapsed_; }
    int elapsed_{0};
};

Session 调用者只看到 advance,不看到 Timer 接口。若不需要 virtual hook,直接包含 Timer 更清晰、耦合更窄。

protected inheritance 把实现关系继续传给后代

保护继承(protected inheritance)把 Base public/protected member 变成 Derived protected,因此 Derived 的后续派生类仍可访问,但普通外部调用者不可访问。它常扩大未来派生层的依赖,实际使用应少且有明确 framework extension contract。

class template 把一族类型写成一个模式

类模板(class template)以类型或非类型参数描述类族。Stack<int, 8>Stack<std::string, 8> 是不同具体类型;模板定义通常必须在实例化点可见,因此常放头文件。

#include <array>
#include <cstddef>
 
template <typename T, std::size_t Capacity>
class Stack {
public:
    bool push(const T& value) {
        if (size_ == Capacity) return false;
        items_[size_++] = value;
        return true;
    }
 
    bool pop(T& out) {
        if (size_ == 0) return false;
        out = items_[--size_];
        return true;
    }
 
private:
    std::array<T, Capacity> items_{};
    std::size_t size_{0};
};

表示 std::array<T,...> items_{} 隐含 T 可默认构造,push/pop 隐含可复制赋值。模板参数不是“任意类型”;真正要求来自所有被实例化的表达式和表示。可通过不同表示、移动 overload 或现代 constraint 收紧诊断。

先预测四种 T 是否能实例化并指出失败表达式,再切换实验面板核对。只说“模板不支持该类型”不够,必须定位是默认构造、复制、比较还是其他操作要求。

模板成员也需要明确可见性与生成时机

编译器只为实际需要的模板成员生成代码;某个类型可以实例化类,却在调用特定成员时失败。模板声明与定义分离若让定义在实例化点不可见,会产生编译或链接问题,可使用显式实例化管理有限类型集合。

Stack<int, 4> ints;
ints.push(7);
 
Stack<std::string, 4> words;
words.push("reuse");

两个实例化各有独立静态成员和具体函数代码语义。模板复用减少源码重复,不代表运行时只有一个万能类。

multiple inheritance 同时组合多个基类子对象

多重继承(multiple inheritance, MI)让一个派生类拥有多个直接基类子对象。它可组合彼此正交的公开协议,但会带来同名成员、转换歧义、构造顺序和重复祖先问题。

class Printable {
public:
    virtual ~Printable() = default;
    virtual void print(std::ostream&) const = 0;
};
 
class Persistable {
public:
    virtual ~Persistable() = default;
    virtual void save(std::ostream&) const = 0;
};
 
class Document : public Printable, public Persistable {
public:
    void print(std::ostream&) const override;
    void save(std::ostream&) const override;
};

当基类协议正交、无共享状态时,接口型 MI 较容易推理。构造直接基类仍按声明顺序,析构逆序;不要靠初始化列表书写顺序猜测。

同名成员与最终覆盖需要显式消歧

若两个基类都有 open()doc.open() 可能歧义;可在派生类提供统一 open,或用 doc.Reader::open() 限定。若两个分支都 override 同一 virtual base function,most-derived class 还需提供唯一 final overrider。

class Scanner { public: void reset(); };
class Printer { public: void reset(); };
 
class Device : public Scanner, public Printer {
public:
    void reset() {
        Scanner::reset();
        Printer::reset();
    }
};

消歧不是任选一个编译通过的名字,而是定义 Device 对外 reset 的领域语义。若两个基类职责冲突,结构本身可能需要拆分。

virtual base 让菱形共享一个祖先子对象

菱形继承中 Left 和 Right 都来自 Root,普通 MI 会让 Bottom 含两个 Root subobject,向 Root 转换和成员查找歧义。把 Root 声明为 virtual base 后,完整 Bottom 只含一个共享 Root;most-derived class 负责初始化它。

class Worker { public: explicit Worker(std::string name); };
class Singer : virtual public Worker { /* ... */ };
class Waiter : virtual public Worker { /* ... */ };
 
class SingingWaiter : public Singer, public Waiter {
public:
    explicit SingingWaiter(std::string name)
        : Worker{std::move(name)}, Singer{}, Waiter{} {}
};

virtual base 改变对象布局、构造责任和转换路径,不会自动消除 Singer/Waiter 自己的同名函数。必须分别审计共享身份与接口冲突。

三步选择并验证复用机制

分步1 / 3

第一步:写关系和对外表面

对每个候选记录 is-a、has-a 或 implemented-in-terms-of,以及调用者/后续派生类真正应看到的操作。

小结

  • containment 表达 has-a 并默认收窄接口,是实现复用的首选起点
  • private/protected inheritance 是实现关系,不等价于 public is-a
  • class templates 生成具体类型,实例化要求来自表示和实际使用的表达式
  • multiple inheritance 可组合正交协议,也引入同名、转换与生命周期歧义
  • virtual base 共享菱形祖先并把初始化责任交给 most-derived class,但不会自动解决接口冲突

练习

  1. 问题 1:选择复用机制。 Window 需要日志能力但不应被当作 Logger,且无需覆盖 Logger virtual hook,应选什么?
  1. 问题 2:定位模板失败。 Stack<unique_ptr<int>>push(const T&) 失败,为什么类模板并非完全不可用?
  1. 问题 3:审计菱形。 virtual Worker 只有一份后,为什么 SingingWaiter 仍可能需要 override show()

名词解释

名词解释

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

包含
以成员表达 has-a 并由外层类选择暴露能力的复用方式。
私有继承
隐藏基类公开替换、用于实现复用的继承方式。
保护继承
把基类能力留给后续派生实现使用的继承方式。
类模板
由类型或常量参数描述一族类的代码模式。
模板实例化
用具体实参生成并检查具体类/成员代码的过程。
多重继承
一个派生类同时含多个直接基类子对象的机制。
虚基类
菱形中共享并由最派生类初始化的基类子对象。

资料与写作方式声明

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

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

讨论

评论区加载中…