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,在强制选择与代码复用间保持清晰边界
Interface / implementation inheritance mapdeclaration syntax is a policy decision, not a convenience defaultinheritance of interfacepure virtualconcrete derived 必须 overrideinheritance of implementationimpure virtual只继承 universally safe defaultnon-virtualfixed public algorithmNVI hook 只定制步骤先问:遗漏 override 应该怎样失败?pure → compile failimpure → default contractnon-virtual → no customization共享 body 也要显式复用;默认不安全时,选择必须在 subtype author 处可见
pure virtual 让选择变成编译期责任,impure virtual 只承诺安全默认,non-virtual 则锁定公共流程并把变化收进 hook。

继承政策实验

先预测:遗漏 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 适合“所有 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 会深入。

三种声明是设计决策,不是默认语法

  • 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 确认默认是否适用。

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。

重复一行 forwarding 的成本远低于错误飞行协议。

pure virtual 也可以拥有 definition

C++ 允许为 pure virtual function 提供 out-of-class body;它仍是 pure,concrete derived 仍必须 override。

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 Interface {
public:
    virtual ~Interface() = 0;
};
 
Interface::~Interface() = default;

普通 pure virtual 无需 body,除非显式 qualified use;destructor 是结构性例外。

default implementation 必须满足所有 subtype

impure virtual 合理条件是:默认对所有当前与可预见 derived 都正确,只是某些 derived 能提供更佳/特化行为。

例如日志 hook 的 default no-op 是否安全取决于审计要求;如果所有操作必须记录,no-op 就不是合法默认。

默认应由 contract 证明,不由“多数实现相同”投票决定。

non-virtual 用于固定算法和不变量

当 base 必须统一执行前置检查、锁、统计或后置验证,可用 non-virtual public function 包装 virtual 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 可分离“谁能调用”和“谁能定制”。

derived 可以 override private virtual,即使不能直接调用它;access 检查与 overriding 是不同机制。

default helper 也要有稳定 contract

shared implementation 不是“内部随便写”。ModelA/B 依赖 defaultFly 的前置、后置、异常和性能。

helper 改动可能影响多个 subtypes,需要共享 contract tests;若 subtypes 开始在 helper 前后堆补丁,可能应抽 strategy/composition。

实现复用仍是 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 定制步骤

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:inheritance of interface 与 inheritance of implementation 的 Airplane 政策。 ModelA/B 共用 default,ModelC 必须自定义,但当前忘记 override 仍可编译。
  1. 问题 2:pure virtual、impure virtual、non-virtual 的 Shape 政策。 draw 无通用实现;reportError 有对所有 shape 安全默认;objectId 必须统一,分别选择。
  1. 问题 3:inheritance of implementation 与 NVI 的固定流程。 Task::run 必须统一验证、审计和错误转换,但执行算法随 subtype 变化。

讨论

评论区加载中…