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
查名与重载实验
先预测: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 中 mf1 和 mf3 会遮掩 Base 中所有同名 overloads。
Item 33 的原则是 Avoid hiding inherited names(避免遮掩继承名称)。
↡编译器按 scope 搜索给定 identifier,并建立当前可见声明集合的阶段。查名发生在重载解析之前
调用 derived.mf3(3.2) 时,compiler 不是把 Base/Derived 所有 mf3 放在一起选最佳匹配。它先查名字:在 Derived 找到 mf3(),于是停止向 Base 查找。
Derived::mf3() 不接受 double,于是调用失败;Base::mf3(double) 根本没有进入候选集合。
↡某个 scope 一旦找到目标名称,普通 member lookup 不继续搜索 base scopes 的规则。这个规则防止较远作用域意外混入 overload set,但 inheritance API 若未显式恢复名称就会产生惊讶。
参数不同也会被遮蔽
hiding 只比较名字,不比较 signature、return type 或 virtual 属性。
↡同一 scope 中同名但参数列表不同、共同参与 overload resolution 的 declarations。 ↡derived virtual function 以兼容 signature 替换 base virtual dispatch target。 ↡内层同名声明让外层同名集合不可见,与签名是否匹配无关。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。
↡derived 通过 using 把 base public/protected name 放入自己指定访问区的行为。class Adapter : private Base {
public:
using Base::start;
};private inheritance 默认让 Base public members 对 Adapter 客户不可见,这条 using 可选择公开 start 的整个 overload family。
↡根据 derived public contract 有意选择哪些 base capabilities 对外可见。公开前要验证这些 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。
↡wrapper 在参数、返回值、异常、value category 与副作用上维持被转交函数契约。decltype(auto) get() & noexcept(noexcept(Base::get())) {
return Base::get();
}简单业务 overload 应写具体签名;generic forwarding 需要 constraints 防止抢占其他 overloads。
↡过宽 forwarding template 成为比预期函数更佳匹配,改变 overload resolution 的风险。只有选择性暴露确实需要 body 时才使用 wrapper,完整恢复优先 using。
base overload 新增会影响 using 客户
using Base::f 自动让未来新增的可访问 Base::f overload 进入 Derived scope。这可保持完整 base interface,也可能改变 overload resolution。
↡base 后续新增同名 overload 后,derived using 自动扩展可见候选集合的行为。例如新增 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 可以同时发生,不能只修其中一个。
↡template 定义与实例化分别执行名称解析的 C++ 模板规则。本章先建立 scope/hiding 心智模型,模板层次再加 dependent context。
name hiding 有时是有意设计
若 Derived 不满足某些 Base overload contract,真正问题可能是 public inheritance 不符合 is-a。隐藏名称不是可靠的权限控制,因为可通过 Base reference/qualification 访问。
↡derived 故意不在自身 unqualified interface 显示某个 base name,但并未删除 base subobject operation。若 public is-a 成立,通常应保留完整 base interface;若只需复用实现,应考虑 private inheritance/composition。
↡用 name hiding 模拟删除 inherited operation,却无法改变 base 可替换接口的设计错误。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 都需调用矩阵验证
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 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:修复 Derived overload。 Base 有
f()、f(int)、f(double),Derived overridef()后其他调用失败,请解释和修复。
- 问题 2:选择性公开 overload。 Adapter private inherit Base,只想公开
process(int),不允许 string/double 版本,设计接口。
- 问题 3:处理 base 版本演进。 Base 新增
f(long)后 Derived using 客户从f(double)改选或产生 ambiguity,如何防回归?