Item 43:学会访问模板化基类内的名称
对齐 Effective C++ 第三版 Item 43:复现 dependent base 中未限定成员查找失败,解释 specialization 与 two-phase lookup 的根因,并比较 this、using declaration 和 explicit qualification 三种访问方式。
学习目标
- 能解释为什么 derived template 的 unqualified call 不会在定义期搜索 dependent base,并复现 two-phase lookup 错误
- 能比较
this->、using declaration 与 explicit qualification 的查找、overload 和 virtual dispatch 行为 - 能设计 primary template 与 specialization 的接口测试,判断 dependent base 成员是否对具体参数真实存在
两阶段查找实验
先预测:这个名字在哪个时刻才会可见?
切换同一个 LoggingMsgSender 的访问策略,观察 definition context、 instantiation context 与 virtual dispatch 如何一起决定结果。
当前判断
错误:primary template 的成员不能替 specialization 做保证。
从一个看似普通的继承调用开始
template<class Company>
class MsgSender {
public:
void sendClear(const MsgInfo& info) {
Company company;
company.sendCleartext(makeMessage(info));
}
void sendEncrypted(const MsgInfo& info) {
Company company;
company.sendEncrypted(makeMessage(info));
}
};派生类想在发送前后记录日志:
template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
logStart(info);
sendClear(info); // error: name not found
logEnd(info);
}
};对非模板继承,这个调用通常可找到 base member;但 MsgSender<Company> 是 dependent base,定义 LoggingMsgSender 时 Company 未知。
Item 43 的原则是 Know how to access names in templatized base classes(学会访问模板化基类名称)。
先预测:若 primary MsgSender 明明声明了 sendClear,compiler 为什么不能直接接受?答案在 specialization,而不是 compiler 看漏了代码。
specialization 可以改变 base interface
class CompanyZ {
public:
void sendEncrypted(std::string_view message);
// policy forbids cleartext
};
template<>
class MsgSender<CompanyZ> {
public:
void sendEncrypted(const MsgInfo& info) {
CompanyZ{}.sendEncrypted(makeMessage(info));
}
};LoggingMsgSender<CompanyZ> 的直接 base 是完全特化版本,它没有 sendClear。如果 compiler 在 derived definition 中无条件采用 primary template 的成员,就会错误接受一个并非对所有 Company 成立的假设。
这不是鼓励 specialization 随意改变 contract,而是解释语言规则为何保守:template definition 必须面对这种合法可能。
two-phase lookup 的两个时刻
↡模板中的名称分别在 definition context 和 instantiation context 进行查找与验证的模型。第一阶段读取 template definition。compiler 查找 nondependent names,尽早发现与任何实参无关的错误。sendClear(info) 的函数名未写成依赖形式,因此不会自动深入 dependent base。
第二阶段在 concrete Company 实例化时处理 dependent names,加入 specialization、ADL 和实参相关上下文,再验证 expression。
↡模板首次被解析时可确定的作用域、声明和非依赖名称环境。 ↡template arguments 已知后用于解析 dependent names 的具体环境。LoggingMsgSender<CompanyA> senderA;
senderA.sendClearMsg(info); // base has sendClear
LoggingMsgSender<CompanyZ> senderZ;
// senderZ.sendClearMsg(info); // specialization has no sendClear把查找延迟并不会保证成功;它只是让成功或失败依据真实 specialization 决定。
修复一:通过 this 制造依赖
template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
logStart(info);
this->sendClear(info);
logEnd(info);
}
};this 的类型依赖 Company,整个 member access expression 因而 dependent。实例化 CompanyA 时查到 MsgSender<CompanyA>::sendClear;CompanyZ 则正确失败。
如果 sendClear 是 virtual,this->sendClear 仍执行正常 virtual dispatch,因此通常是表达“调用当前对象接口”的首选。
修复二:using declaration 引入 base 名称
template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
using MsgSender<Company>::sendClear;
public:
void sendClearMsg(const MsgInfo& info) {
sendClear(info);
}
};using 让 unqualified lookup 在 derived scope 找到该名称。若 base 有多个 sendClear overload,名称对应的 overload set 会参与选择。
using 的访问位置决定客户是否能直接看到该名称;放在 private 区域可只服务内部查找,放在 public 区域可能改变 derived API。
修复三:explicit qualification 指定 base
template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
MsgSender<Company>::sendClear(info);
}
};这种写法最明确地选择 base implementation,适合确实要绕开 override 的内部调用。但如果 sendClear 是 virtual,qualified call 会抑制 virtual dispatch。
↡通过 qualified member call 固定 base implementation,不再进行运行期 override 选择。因此三种写法都能解决查找,却不完全等价。
按意图选择三种写法
- 要调用当前对象语义并保留 virtual dispatch:优先
this->name(args)。 - 要让一组 base overload 在 derived scope 长期可见:使用
using Base<T>::name。 - 要固定调用某个 base implementation:使用
Base<T>::name(args),并明确接受绕过 virtual dispatch。
不要为消除编译错误随便选最短写法;先回答是否允许更派生 override 接管。
this 也会改变 overload 查找范围
void sendClear(int retries);
template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void test(const MsgInfo& info) {
this->sendClear(info);
}
};若 derived 自己声明同名函数,普通 unqualified lookup 可能隐藏 base overload。this-> 让 member lookup 发生在当前实例,但 overload set 的具体形成仍需结合 hiding 规则;using 是明确恢复 base overloads 的工具。
using MsgSender<Company>::sendClear;
void sendClear(int retries);此时两个 signatures 可共同参与 overload resolution。
不是所有 base 都 dependent
class AuditBase {
protected:
void logStart(const MsgInfo&);
};
template<class Company>
class LoggingMsgSender : public AuditBase, public MsgSender<Company> {
void send(const MsgInfo& info) {
logStart(info); // nondependent base, found now
this->sendClear(info); // dependent base, deferred
}
};判断依据是 base type 是否依赖 template parameters,而不是 derived 自己是不是 template。
用 concepts 检查 specialization contract
现代 C++ 可在入口处表达某个 sender 是否支持 cleartext,但 concept 不能替代正确的 dependent lookup。
template<class Sender>
concept ClearSender = requires(Sender& sender, const MsgInfo& info) {
sender.sendClear(info);
};
static_assert(ClearSender<MsgSender<CompanyA>>);
static_assert(!ClearSender<MsgSender<CompanyZ>>);若 LoggingMsgSender 本质上只适用于 ClearSender,可在 class 或 member function 上加约束,把 CompanyZ 的错误移到更清晰的边界。
↡把只对部分 template arguments 成立的成员能力写进 requires 条件的边界。一套可执行诊断流程
遇到 derived template 报“名称未声明”时:
- 判断名称是否 unqualified 且来自 base。
- 判断 base type 是否依赖 template parameters。
- 检查 primary template 是否可能有 partial/full specializations。
- 决定意图是动态当前对象、引入 overload set,还是固定 base implementation。
- 分别选择
this->、using declaration 或 explicit qualification。 - 用至少一个普通参数和一个接口不同的 specialization 编译验证。
先预测 CompanyA 与 CompanyZ 的实例化结果,再读 compiler diagnostic 中 definition phase 和 instantiation phase 的位置。
小结
- dependent base 的成员集合可能因 specialization 改变,定义期不能假设 primary template 成员存在
- two-phase lookup 在定义期处理 nondependent names,在实例化期处理 dependent names
this->name制造依赖并保留正常 virtual dispatch- using declaration 引入 base 名称或 overload set,也可能影响 derived API
- explicit qualification 固定 base implementation,virtual call 会被限定而不再动态分派
- 修复语法必须匹配调用意图,并用不同 specializations 验证真实 contract
名词解释
本章出现的专业名词,用大白话再讲一遍。
- dependent base class
具体类型依赖模板参数的基类。
- templatized base name
来自 dependent base、需延迟确认的名称。
- full specialization
为完整实参组合提供的独立模板定义。
- specialization interface divergence
主模板和特化成员集合不一致。
- two-phase lookup
定义期与实例化期分开查找名称。
- definition context
解析 template definition 时已知的环境。
- instantiation context
具体模板实参已知后的查找环境。
- dependent this lookup
经 this 延迟搜索 dependent bases。
- preserved virtual dispatch
this 调用继续选择运行期 override。
- using declaration
把指定 base 名称引入 derived scope。
- inherited overload set
引入后共同参与重载选择的函数集合。
- explicit qualification
显式写出目标 dependent base scope。
- virtual dispatch suppression
限定调用绕过运行期 override 选择。
- lookup-remedy intent
按分派和可见性目的选择查找修复。
- derived name hiding
derived 同名声明隐藏 base overloads。
- nondependent base class
定义期成员集合已知的基类。
- specialization contract test
分别验证主模板和特化能力的检查。
- capability-constrained derived template
按成员能力限制适用实参的派生模板。
- dependent-base diagnostic workflow
依赖性到实例化验证的定位步骤。
练习
- 问题 1:修复 LoggingMsgSender。 要保留未来 derived override sendClear 的能力,并让 CompanyZ 清晰失败。
- 问题 2:derived 新增 sendClear(int) 后,MsgInfo 调用失败。 要同时保留 base overloads。
- 问题 3:必须执行 base 审计实现,不允许更深 override 改变。 说明调用和测试。