Item 36:绝不重新定义继承而来的 non-virtual 函数

对齐 Effective C++ 第三版 Item 36:证明 non-virtual 函数按静态类型绑定,derived 同名声明不是 override,会让同一对象经 Base pointer 与 Derived pointer 表现不同,并选择 virtual、NVI、改名或组合修复。

学习目标

  • 能复现同一 Derived object 经 Base pointer 与 Derived pointer 调用不同 non-virtual bodies
  • 能解释 non-virtual 声明表示接口加强制实现,derived 同名函数为何破坏 public is-a
  • 能判断保持 base body、改成 virtual/NVI、改名、composition 或静态 policy 的修复边界
Static binding / view split mapone derived object → two static views → two selected bodiesderived objectdynamic type = Derived同一地址 / 同一状态base pointerstatic type Base* → Base::mfderived viewstatic type Derived* → Derived::mfredefine an inherited non-virtual functionstatic binding ≠ overridepublic is-a splitdelete / virtual-NVIcomposition / rename先写 static type,再承诺 selected bodydynamic type 只有在 virtual dispatch 中才决定最终实现
同一对象的两个静态视图不应产生两个 public 语义;需要变化时把 variation 写进 virtual/NVI 或改用组合。

dispatch 诊断实验

先预测:这次调用会落到哪个 body?

先写出表达式的 static type 和对象的 dynamic type,再切换视图查看 dispatch 证据。

观察

同一个 Derived object 同时被 Base* 与 Derived* 观察;两个表达式看似调用同一个 mf,实际可能选不同 body。

决策

Base* 视图必须继续满足 Base contract;不能要求所有调用者“记得”使用 Derived* 才得到业务语义。

当前诊断 · base pointer / derived object

Base* 视图必须继续满足 Base contract;不能要求所有调用者“记得”使用 Derived* 才得到业务语义。

从同一个对象出现两种行为开始

class Base {
public:
    void mf() const {
        log("Base::mf");
    }
};
 
class Derived : public Base {
public:
    void mf() const {
        log("Derived::mf");
    }
};

Derived::mf 看起来像 override,但 Base::mf 不是 virtual。两个 calls 的结果由表达式 static type 决定:

Derived object;
 
Base* baseView = &object;
Derived* derivedView = &object;
 
baseView->mf();    // Base::mf
derivedView->mf(); // Derived::mf

Item 36 的原则是 Never redefine an inherited non-virtual function(绝不重定义继承 non-virtual 函数)。

static type 与 dynamic type 分工不同

baseView 的 static type 是 Base*,dynamic object 是 Derived。virtual function 会按 dynamic type dispatch;non-virtual 早在 compile time 选定 Base::mf。

只改变指针声明,就让同一 object 执行不同语义。这不是正常 polymorphism。

non-virtual 表示行为必须统一

Item 34 已建立:public non-virtual function 同时继承 interface 与 mandatory implementation。Base author 在说“每个 Base subtype 都应这样做”。

如果 Derived 确实 is-a Base,那么 Base::mf 的 contract 对 Derived 也必须成立。Derived 不能再用同名入口声明另一个政策。

同名声明只是 hiding,不是 overriding

Derived::mf 会在 Derived scope 隐藏 Base::mf;它没有 vtable slot,也不参与 virtual override。

override 能把误解变成 compile error:

class Derived : public Base {
public:
    void mf() const override; // error: Base::mf is not virtual
};

所有 intended override 都应写 override;这能捕获 base 忘记 virtual、签名漂移和 const mismatch。

using declaration 不能改变 binding

using Base::mf 只改变 name visibility/overload set,不会把 Base::mf 变 virtual,也不会让 Derived::mf 动态分派。

class Derived : public Base {
public:
    using Base::mf;
    void mf() const; // same signature still hides/selects in Derived scope
};

Item 33 解决“哪些 names 可见”,本章解决“call body 何时确定”;两层不能互相替代。

public is-a 因视图变化而破坏

process(Base&) 是合法 base client,它调用 mf 时必须得到 Base contract。Derived direct client 得到另一 contract,意味着 Derived 不再有单一可替换语义。

void process(const Base& value) {
    value.mf();
}
 
Derived d;
process(d); // Base::mf
d.mf();     // Derived::mf

这正违反 Item 32 的 behavioral substitutability。

若所有 subtype 行为相同,删除 derived 函数

最简单修复是继承 Base implementation:

class Derived : public Base {
    // no mf declaration
};

如果 Derived 只想在调用前后做额外工作,应使用不同名称的 operation 或由调用者显式组合,不能悄悄改变 inherited mf。

名称应表达新 contract,而不是复用 mf 制造隐藏。

若行为应随 subtype 变化,base 必须声明 virtual

如果 Base contract 本来就允许 customization,修改 base:

class Base {
public:
    virtual ~Base() = default;
    virtual void mf() const = 0;
};
 
class Derived : public Base {
public:
    void mf() const override;
};

这会改变 ABI、destructor/ownership 和 performance contract,公共库需版本策略。

NVI 保留统一外壳并允许步骤变化

如果 public mf 的 validation、锁、审计必须一致,只让核心步骤变化:

class Base {
public:
    void mf() const {
        validate();
        doMf();
        audit();
    }
 
private:
    virtual void doMf() const = 0;
};

Derived override doMf,不声明 mf;既保持 base invariant,又提供动态算法。

不能修改 legacy base 时用组合或 adapter

第三方 Base 的 mf non-virtual,业务需要不同 operation,不能安全 public derive 后同名重定义。

class SpecializedService {
public:
    void specializedOperation() {
        base_.mf();
        applySpecialRules();
    }
 
private:
    Base base_;
};

客户不会把 SpecializedService 当 Base,因此没有 static-view split。

qualified base call 是 virtual override 内的显式复用

Derived virtual override 可以故意调用 Base::mf 复用 base body;qualified call 明确绕过 dynamic dispatch。这与重定义 non-virtual 不同。

void Derived::mf() const {
    Base::mf();
    extendBehavior();
}

前提是 Base::mf 本身 virtual customization contract 合法;qualified call 不能把 nonvirtual split 变正确。

final 可表达不允许继续变化

若 virtual hierarchy 到某层需要固定 implementation,可将 override 标 final:

void mf() const final;

这比把同名 non-virtual function 塞进 derived 更清楚,compiler 能检查。

如果整个 class 不应被继承,可声明 class final。

CRTP/static polymorphism 是不同契约

CRTP 可让 base template 静态调用 derived implementation:

template<class Derived>
class StaticBase {
public:
    void mf() const {
        static_cast<const Derived&>(*this).doMf();
    }
};

每个 StaticBase<Derived> 是不同类型;它不承诺把任意 derived object 经一个非模板 Base* runtime 替换。

因此 static polymorphism 不等于“在普通 public inheritance 中重定义 nonvirtual 合法”。

overload 与同签名 redefinition 要分开

Derived 新增 mf(int) 会隐藏 Base::mf(),属于 Item 33 的 name lookup 问题;Derived 新增相同 mf() 则同时产生本章行为分裂。

using 可修复前者的可见性,无法修复后者的 dispatch 语义。

用双视图测试固定行为

先预测每个 call 的 static type 与 selected body,再建立门禁:

  • 对同一 Derived object 通过 Base*/Base&/Derived*/Derived& 调用 operation。
  • compile check 要求 intended customization 使用 override;nonvirtual 上 override 必须失败。
  • 搜索 derived 与 base 同签名 non-virtual declarations,作为 lint error。
  • base contract suite 经每个 subtype factory 重跑。
  • virtual repair 后所有 views 命中 Derived override,并覆盖 virtual destructor。
  • NVI repair 证明所有 views 经过 wrapper,hook 前后 invariant 不遗漏。
  • composition adapter compile test 证明不能隐式转换为 legacy Base。
  • ABI tests 覆盖把 public base function 改 virtual 的版本影响。

只有所有合法 views 观察到同一 public contract,继承设计才成立。

小结

  • non-virtual function 静态绑定,selected body 由 pointer/reference/expression static type 决定
  • derived 同名同签名 function 只是 hiding/redefinition,不是 override
  • 同一 Derived object 经 Base* 调 Base body、经 Derived* 调 Derived body,破坏 is-a substitutability
  • using declaration 只能恢复名称可见性,不能把 nonvirtual 变成 dynamic dispatch
  • 行为不变就删除 derived function;合法变化就让 base virtual 或采用 NVI
  • legacy base 不可改时使用 composition/adapter,static polymorphism 则是另一套显式 compile-time contract

资料与写作方式声明

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

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

名词解释

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

static binding

按表达式静态类型选择 nonvirtual body。

non-virtual redefinition

derived 同签名声明形成另一静态入口。

static type

编译期决定 lookup 和静态绑定的类型。

dynamic type
运行期实际最派生对象类型。
base pointer view

经 Base pointer/reference 观察 derived object。

derived object view

保留 Derived 静态类型的调用视图。

mandatory inherited implementation

全部 subtype 统一使用的 base nonvirtual 政策。

is-a behavior preservation

derived 保持全部 base 客户行为。

non-virtual name hiding

同名 derived 声明隐藏 base 名称。

override contract check

compiler 验证真实 virtual override。

visibility-only using declaration

using 只改可见性不改 binding。

lookup-dispatch confusion

以查名工具误修 dispatch 问题。

static-view behavior split

不同静态视图产生不同语义。

substitution inconsistency

base 与 concrete 客户观察不兼容结果。

inherited policy acceptance

derived 接受 base nonvirtual 实现。

distinct derived operation

以不同名称表达新 contract。

intentional virtual variation

base 明确声明的动态 customization。

dynamic-view consistency

所有静态 views 命中同一 override。

NVI repair

固定 public wrapper 加 virtual hook 的修复。

uniform public entry

所有调用经过同一 nonvirtual 外壳。

legacy composition adapter

持有并委托不可修改 base 的 wrapper。

composition repair

以 has-a 复用而不声明 is-a。

qualified base reuse

override 中显式调用 base body。

override composition

动态 override 内组合 base implementation。

final override boundary

禁止进一步 override 的终点。

checked dispatch policy

virtual/override/final 的可检查政策。

static polymorphism

template static type 驱动的多态。

explicit static dispatch contract

不依赖统一 runtime Base* 的静态契约。

derived overload hiding

不同参数同名 derived function 的遮蔽。

same-signature static split

同签名 nonvirtual 形成两个静态 bodies。

static-binding dispatch matrix

静态/动态类型与选中 body 的测试表。

练习

  1. 问题 1:static binding 与 base pointer / derived object 双视图。 Base/Derived 都声明 nonvirtual normalize(),同一 Derived object 经两种 pointers 调用,请解释并修复。
  1. 问题 2:redefine an inherited non-virtual function 与第三方 Base。 业务需要扩展 mf,但库的 Base::mf nonvirtual,团队计划 public derive 同名覆盖。
  1. 问题 3:static binding、using 与 CRTP。 团队认为 using Base::mf 可修复同签名 redefinition,另有人用 CRTP 证明 nonvirtual 可多态,请澄清。

讨论

评论区加载中…