Item 33:避免遮掩继承而来的名称

对齐 Effective C++ 第三版 Item 33:解释 C++ 先名称查找后重载解析导致的 inherited overload hiding,区分 hiding/overloading/overriding,并用 using declaration 或选择性 forwarding function 恢复接口。

学习目标

  • 能解释 name lookup 在 derived scope 找到同名声明后为何停止,随后 overload resolution 才开始
  • 能区分 name hiding、overloading、virtual overriding,并用 override 与 using declaration 恢复完整接口
  • 能设计 forwarding function,在 private inheritance 或窄接口中只暴露目标 base overload
Inherited name lookup:先查名,再选 overloadhiding inherited names is a visibility decision, not an overload decisionDerived scopemf3()同名声明在内层出现Base scopemf3()mf3(double) ← 被遮掩Base 候选尚未进入集合name lookup1. 找到 Derived::mf32. stop rule:停止向 Base 查找先建立可见名称集合不比较参数列表不比较 return type也不因 virtual 自动合并overload resolutionmf3(3.2)candidate: Derived::mf3()no matching functionBase::mf3(double)未进入候选集合overload 解析来得太晚修复策略:明确要恢复多少接口using declaration → 完整 overload familyforwarding function → 一个窄 signature
查名先于重载解析:要么用 using declaration 恢复整个可访问名称族,要么用 forwarding function 有意只暴露一个签名。

查名与重载实验

先预测:derived.mf3(3.2) 会找到谁?

先写下候选集合,再切换修法;观察 using declaration 恢复完整接口,或 forwarding function 收窄公开契约。

当前表达式

derived.mf3(3.2)

可见候选

Derived::mf3()

设计判断

Derived scope 先找到 mf3;Base::mf3(double) 没有进入候选集合,因此调用失败。

当前修法 · hiding inherited names / 未恢复

Derived scope 先找到 mf3;Base::mf3(double) 没有进入候选集合,因此调用失败。

从 base overload 明明存在却调用失败开始

Base 提供同名函数族:

class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
 
    void mf3();
    void mf3(double);
};

Derived 定制其中两个名字:

class Derived : public Base {
public:
    void mf1() override;
    void mf3();
};

看似只增加/覆盖两个签名,实际 Derived scope 中 mf1mf3 会遮掩 Base 中所有同名 overloads。

Item 33 的原则是 Avoid hiding inherited names(避免遮掩继承名称)。

查名发生在重载解析之前

调用 derived.mf3(3.2) 时,compiler 不是把 Base/Derived 所有 mf3 放在一起选最佳匹配。它先查名字:在 Derived 找到 mf3(),于是停止向 Base 查找。

Derived::mf3() 不接受 double,于是调用失败;Base::mf3(double) 根本没有进入候选集合。

这个规则防止较远作用域意外混入 overload set,但 inheritance API 若未显式恢复名称就会产生惊讶。

参数不同也会被遮蔽

hiding 只比较名字,不比较 signature、return type 或 virtual 属性。

Derived::mf3() 会隐藏 Base::mf3(double),即使一个无参数、一个接收 double;Derived::mf1() 会隐藏 Base::mf1(int),即使一个是 override、另一个是不同 overload。

override 只验证覆盖,不恢复重载族

override 能让 compiler 检查 signature 是否真正覆盖某个 base virtual;它不会把其他 base overloads 自动带回 Derived scope。

class Derived : public Base {
public:
    void mf1() override;
};

这能捕获 const/ref/noexcept/signature 错误,但 Base::mf1(int) 仍被同名 mf1 遮蔽。

因此现代 C++ 同时需要 override 和 using declaration,各解决不同问题。

using declaration 恢复整个 base 名称集合

class Derived : public Base {
public:
    using Base::mf1;
    using Base::mf3;
 
    void mf1() override;
    void mf3();
};

现在 Derived 调用者可见 Derived::mf1()Base::mf1(int)Derived::mf3()Base::mf3(double)

using 不复制实现,也不创建 forwarding body;它改变可见性和 overload candidates。

access section 决定重新暴露的位置

using 写在 public/protected/private section,会决定客户从 Derived 看到该名称的 access。它不能把 Base private member 变成可访问;只能处理 Derived 本来可访问的 base declarations。

class Adapter : private Base {
public:
    using Base::start;
};

private inheritance 默认让 Base public members 对 Adapter 客户不可见,这条 using 可选择公开 start 的整个 overload family。

公开前要验证这些 overloads 的 contract 对 Derived 都成立。

只想暴露一个 overload 时使用转交函数

using 会引入指定名字的所有可访问 overloads。若 Derived 只想公开 process(int),应写 forwarding function:

class Adapter : private Base {
public:
    void process(int value) {
        Base::process(value);
    }
};

qualified Base::process(value) 避免 wrapper 递归调用自己,并明确目标。

wrapper 还可转换错误、添加验证或改变 access,但这时它形成新的 Derived contract,不只是名字恢复。

forwarding 要保持参数与返回语义

手写 wrapper 可能漏掉 cv/ref/noexcept、perfect forwarding 或返回 category。

decltype(auto) get() & noexcept(noexcept(Base::get())) {
    return Base::get();
}

简单业务 overload 应写具体签名;generic forwarding 需要 constraints 防止抢占其他 overloads。

只有选择性暴露确实需要 body 时才使用 wrapper,完整恢复优先 using。

base overload 新增会影响 using 客户

using Base::f 自动让未来新增的可访问 Base::f overload 进入 Derived scope。这可保持完整 base interface,也可能改变 overload resolution。

例如新增 f(long) 可能改变 Derived 客户原本选择 f(double) 的表达式。

公共库需用 compile tests 固定关键 calls 的 selected overload,版本说明也要记录。

generated assignment 也有同名遮蔽细节

derived 会拥有自己的 copy/move assignment;即使 using Base::operator=,compiler-generated/special-member rules 仍可能让某些 signatures 被 Derived assignment 隐藏。

class Derived : public Base {
public:
    using Base::operator=;
    Derived& operator=(const Derived&) = default;
};

不要假设一条 using 解决所有 special member 语义;用 compile-time invocability/assignment tests 验证。

multiple inheritance 会增加查名歧义

两个 bases 都声明 log(),Derived 无自身 log 时,调用可能 ambiguous;声明 Derived::log 又会遮蔽两个 base families。

class Service : public FileLogger, public MetricsLogger {
public:
    void log() {
        FileLogger::log();
        MetricsLogger::log();
    }
};

也可重命名 capabilities 或用 composition,避免一个 name 承担不同语义。

template dependent base 是另一层查找问题

class template 的 dependent base names 在第一阶段 lookup 中通常不会自动查找,需要 this->f()using Base<T>::f 或 qualification;Item 43 会深入。

普通 inheritance name hiding 与 dependent-name lookup 可以同时发生,不能只修其中一个。

本章先建立 scope/hiding 心智模型,模板层次再加 dependent context。

name hiding 有时是有意设计

若 Derived 不满足某些 Base overload contract,真正问题可能是 public inheritance 不符合 is-a。隐藏名称不是可靠的权限控制,因为可通过 Base reference/qualification 访问。

若 public is-a 成立,通常应保留完整 base interface;若只需复用实现,应考虑 private inheritance/composition。

Item 32 的 substitutability 检查优先于本章语法修复。

用调用矩阵验证可见集合

先预测每个 expression 在 Derived scope 找到哪些 names,再让 compile tests 验证:

  • 对每个 Derived 同名 declaration,列出被遮蔽的 Base overloads。
  • using Base::f 前后记录 viable candidates 与 selected signature。
  • override tests 覆盖 const/ref/noexcept mismatch,确保 override 真实成立。
  • selective forwarding 只让目标 signature 可调用,其余 overload compile fail。
  • base 新增 overload 后重跑 ambiguity/conversion ranking tests。
  • operator= 测试覆盖 Base/Derived lvalue/rvalue assignment combinations。
  • multiple inheritance 对同名 functions 使用 qualified composition tests。
  • template dependent base 另建 this/using/qualification compile matrix。

名称可见性属于 public API,应由编译测试固定,而不是等用户报 no matching function。

小结

  • C++ 先按 scope 做 name lookup,Derived 找到同名声明后停止,再进行 overload resolution
  • hiding 只看名称,与参数、return type 或 virtual 无关
  • override specifier 检查一个 virtual signature,不会恢复其他 base overloads
  • using Base::name 把完整可访问 overload family 引入 Derived scope
  • 只想暴露一个 overload 时使用 forwarding function 与 qualified base call
  • base overload 演进、special assignment、multiple inheritance 和 dependent base 都需调用矩阵验证

资料与写作方式声明

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

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

名词解释

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

inherited name hiding

derived 同名声明遮蔽全部 base 同名 declarations。

name lookup

按 scope 搜索 identifier 并建立声明集合。

overload resolution

在可见声明中按参数转换选择函数。

scope lookup stop rule

当前 scope 找到名字后停止搜索 base。

function overloading

同 scope 同名不同参数的函数集合。

virtual overriding

derived 替换 base virtual dispatch target。

name hiding
内层同名让外层集合不可见。
override specifier

要求函数真实覆盖 base virtual。

override-only validation

override 不改变其他名称可见性。

using declaration

把 base 名称声明引入 derived scope。

restored overload set

using 后合并的 base/derived 候选集合。

inherited access exposure

按 access section 重新公开 base name。

explicit inherited API selection

有意选择对外 base capabilities。

forwarding function

窄签名 wrapper 转交指定 base overload。

qualified base call

使用 Base::name 明确调用 base implementation。

forwarding contract fidelity

wrapper 保持参数返回异常语义。

forwarding overload capture

过宽 wrapper 抢占预期 overload。

overload-set evolution

base 新 overload 自动进入 using 集合。

overload resolution drift

候选变化使既有调用改选或歧义。

special-member name interaction

generated special members 与 base 名称交互。

assignment overload audit

验证 operator= 可见与选择的测试。

multiple-base name ambiguity

多个 base 同名导致查找歧义。

multi-base forwarding composition

wrapper 组合多个 base implementations。

dependent base class

类型依赖模板参数的 base。

two-phase name lookup

模板定义与实例化阶段的查名规则。

intentional name suppression

derived 有意不显示 base name。

hiding-as-interface-removal smell

以 hiding 假装删除 inherited operation。

inherited lookup matrix

记录可见名称、候选和选择的测试表。

练习

  1. 问题 1:修复 Derived overload。 Base 有 f()f(int)f(double),Derived override f() 后其他调用失败,请解释和修复。
  1. 问题 2:选择性公开 overload。 Adapter private inherit Base,只想公开 process(int),不允许 string/double 版本,设计接口。
  1. 问题 3:处理 base 版本演进。 Base 新增 f(long) 后 Derived using 客户从 f(double) 改选或产生 ambiguity,如何防回归?

讨论

评论区加载中…