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 成员是否对具体参数真实存在
决策地图:先分离名称查找,再选择调用语义
1定义期Company 未知先判定名称是否依赖模板参数2实例化期specialization 已知再检查真实 base 的成员集合3调用意图动态还是固定选择 this->、using 或 qualified call查找边界决定“能否找到”,调用限定决定“谁来执行”

两阶段查找实验

先预测:这个名字在哪个时刻才会可见?

切换同一个 LoggingMsgSender 的访问策略,观察 definition context、 instantiation context 与 virtual dispatch 如何一起决定结果。

two-phase lookup:definition → instantiation → dispatch当前策略:未限定调用definition contextdependent base:MsgSender<Company>primary 与 specialization 未知definition context:名称不依赖 Company,dependent base 不在此处展开。先决定名称是否 dependentinstantiation contextCompanyA / CompanyZ 已知选择真实 base specializationCompanyA 到达实例化时已经太晚:模板定义阶段就拒绝了 sendClear。延迟不是跳过验证call resultsendClear(info)没有形成可调用表达式错误:primary template 的成员不能替 specialization 做保证。访问语法必须匹配调用意图名称查找 ≠ virtual dispatchthis->:依赖当前对象 · using:引入 overload set · qualified:固定 base先问“谁应该接管调用”,再问“怎样让 compiler 找到名称”没有形成可调用表达式
同一 dependent base 可在不同查找策略下得到不同边界;图中把两阶段查找与 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 的两个时刻

第一阶段读取 template definition。compiler 查找 nondependent names,尽早发现与任何实参无关的错误。sendClear(info) 的函数名未写成依赖形式,因此不会自动深入 dependent base。

第二阶段在 concrete Company 实例化时处理 dependent names,加入 specialization、ADL 和实参相关上下文,再验证 expression。

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。

因此三种写法都能解决查找,却不完全等价。

按意图选择三种写法

  1. 要调用当前对象语义并保留 virtual dispatch:优先 this->name(args)
  2. 要让一组 base overload 在 derived scope 长期可见:使用 using Base<T>::name
  3. 要固定调用某个 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 的错误移到更清晰的边界。

一套可执行诊断流程

遇到 derived template 报“名称未声明”时:

  1. 判断名称是否 unqualified 且来自 base。
  2. 判断 base type 是否依赖 template parameters。
  3. 检查 primary template 是否可能有 partial/full specializations。
  4. 决定意图是动态当前对象、引入 overload set,还是固定 base implementation。
  5. 分别选择 this->、using declaration 或 explicit qualification。
  6. 用至少一个普通参数和一个接口不同的 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

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:修复 LoggingMsgSender。 要保留未来 derived override sendClear 的能力,并让 CompanyZ 清晰失败。
  1. 问题 2:derived 新增 sendClear(int) 后,MsgInfo 调用失败。 要同时保留 base overloads。
  1. 问题 3:必须执行 base 审计实现,不允许更深 override 改变。 说明调用和测试。

讨论

评论区加载中…