Item 42:理解 typename 的双重含义

对齐 Effective C++ 第三版 Item 42:说明 template type parameter 中 class 与 typename 等价,识别 nested dependent type name 的解析歧义,并掌握 typename 在普通声明、base list 和 mem-initializer 中的位置规则。

学习目标

  • 能解释 template type parameter 声明中 classtypename 完全等价,且二者都可接收非 class 类型
  • 能判断一个 qualified name 是否为 nested dependent type name,并写出正确的 typename 消歧
  • 能区分普通声明、alias、base list 与 mem-initializer 的 typename 规则并复现编译结果
typename 的两条路径先看它出现在哪个 grammar context,再决定是否承担消歧职责template parameter listclass T ≡ typename T两者都是 type parameterdependent qualified nameC::const_iterator未知 specialization 可能给它不同含义typename disambiguationtypename C::const_iterator明确把名称当作 type位置例外:grammar 已经提供 type 证据base-specifier:不写mem-initializer:不写function body:通常要写关键不是看到 :: 就添加关键字,而是判断当前位置是否已经锁定为类型语法
typename 既可以是参数列表里的关键词,也可以是 dependent name 的类型消歧提示;上下文决定第二种用法是否需要它。

typename 位置实验

先预测:这个 dependent name 要不要 typename?

选择同一个 dependent base name 出现的位置,观察 grammar 已经提供了多少类型证据。

函数体声明typename C::const_iterator iter;需要 typenamegrammar evidence → typename decision

当前判断

需要 typename

普通声明没有预先保证末尾名称是类型,parser 需要消歧提示。

解释时要说出“为什么”,而不只是背下关键字清单:同一个名字离开 base grammar 进入普通声明后,规则会重新变成 dependent type 的消歧问题。

dependent name 编译诊断流程语法消歧和 specialization 事实验证是两个连续阶段1. qualified?A::B / T::value_type没有 :: 就不走此流程2. dependent?qualifier 依赖 T / C未知 specialization 才会带来歧义3. 要当 type?local / alias → typenamebase grammar → 不加4. instantiatenested type 存在?typename 不会创造类型若尖括号歧义来自 dependent member template,切换到 template disambiguatortypename 说明“这是类型”|template 说明“这是模板”|两者可以出现在同一条复杂名称中
诊断顺序把 parser 的问题和实例化后的事实问题分开:先消歧,再确认 specialization 的契约。

从两段等价声明开始

template<class T>
class Widget;
 
template<typename T>
class Gadget;

在 template parameter list 中,class Ttypename T 意义完全相同:都声明 T 是 type parameter。

Item 42 的原则是 Understand the two meanings of typename(理解 typename 的双重含义)。

Widget<int> countWidget;
Gadget<double> ratioGadget;

class T 并不要求 T 必须是 class;int 同样合法。团队可以用风格约定区分,但不能把约定误当语言限制。

先预测:把 template<class T> 全部改成 template<typename T>,生成代码和 overload 行为是否改变?

第二种含义:声明依赖名称是类型

template<class C>
void printSecond(const C& container) {
    C::const_iterator iter(container.begin()); // parse error
}

C::const_iterator 的含义依赖具体 C。它既是 qualified name,又位于 dependent scope 中,并且嵌套在 C 里面。

正确写法是:

template<class C>
void printSecond(const C& container) {
    typename C::const_iterator iter(container.begin());
}

这里 typename 不是引入 template parameter,而是告诉 compiler:把后面的 qualified dependent name 当作 type。

为什么 compiler 不等实例化后再猜

看这条声明:

C::const_iterator* p;

如果 C::const_iterator 是类型,这是 pointer declaration;如果某个 specialization 把 const_iterator 定义成 static data member,且作用域里已有变量 p,tokens 也可读成 multiplication expression。

C++ 不能让模板定义阶段的 grammar 完全悬空,因此采用保守规则:除非上下文已经确定它是 type,或程序用 typename 指明,否则 nested dependent name 不按 type 解析。

typename C::const_iterator* p;

三问定位法

不要看到 :: 就机械添加 typename,依次问:

  1. 名称是否 qualified,例如 A::B
  2. qualifier 是否依赖 template parameter,例如 Container<T>T
  3. 最终名称是否要在当前上下文被当作 type?

三项都成立时,通常需要 typename

template<class Iter>
void clearValue(Iter iter) {
    typename std::iterator_traits<Iter>::value_type value{};
    *iter = value;
}

value_type 是 iterator_traits 的 nested name,iterator_traits specialization 取决于 Iter,当前要把它声明为 local variable type,所以需要 typename。

已知类型与非依赖名称不需要 typename

std::string::size_type length = 0;

std::string 不依赖当前 template parameter,compiler 在定义点已经知道 size_type 是类型。

template<class T>
struct Holder {
    using Index = std::size_t;
    Index index = 0;
};

当前 template 自己已建立的 member alias 可在其已知上下文中直接使用。关键不是“代码位于模板中”,而是名称含义是否依赖未知 specialization。

alias 声明中的依赖类型

template<class C>
using Element = typename C::value_type;
 
template<class C>
void consume(const C& source) {
    using Iter = typename C::const_iterator;
    Iter first = source.begin();
}

alias 只能把重复的 dependent name 和 typename 集中起来;它不会取消最初声明处的消歧责任。建立 Element<C> 后,使用 alias template 的结果本身已处于类型语法上下文,调用代码更清楚。

base list 是明确例外

template<class T>
class Derived : public Base<T>::Nested {
    // ...
};

base-specifier grammar 已要求这里出现 base type,所以不写、也不能按 Item 42 的规则写 typename

template<class T>
class Derived : public Base<T>::Nested {
public:
    explicit Derived(int value)
        : Base<T>::Nested(value) {}
};

constructor mem-initializer 中指定 dependent base class 时同样不加 typename。

同一个名称进入函数体后规则改变

template<class T>
class Derived : public Base<T>::Nested {
public:
    void makeCopy() {
        typename Base<T>::Nested copy(*this);
    }
};

在 base list 中 grammar 已确定 type;进入普通 statement 后,Base<T>::Nested 又可能是 type 或 value,因此 local declaration 需要 typename。

这说明记忆“某个名字总要 typename”是错误模型。应记住 parser 在这个位置是否已经知道它必须是 type。

typename 不能证明 specialization 真的提供类型

typename 是解析指令,不会创造类型。

template<class T>
struct Traits;
 
template<class T>
using Value = typename Traits<T>::value_type;

Traits<int> 没有 value_type,alias 在实例化时仍失败。

与 dependent template 的对照

当 dependent object 后的名称是 member template,另一个消歧关键字是 template

template<class T>
void invoke(T& object) {
    object.template convert<int>();
}

typename 解决“后面是否是类型”,template 解决“后面是否是模板”。两者可组合:

typename Registry<T>::template Rebind<int>::type value;

先确认 Rebind 是 template,再确认最终 type 是 dependent type。复杂声明应优先用 alias 分段,降低解析和审查负担。

一套编译诊断流程

遇到 dependent name 报错时:

  1. 标出所有 template parameters 和 dependent expressions。
  2. 对每个 A::B 执行 qualified-dependent-type test。
  3. 若是普通声明/alias 的 dependent type,在名称前加 typename。
  4. 若位于 base list 或 base mem-initializer,确认不写 typename。
  5. 若尖括号被解析成比较运算,检查是否缺 dependent template disambiguator。
  6. 用至少两个 specializations 编译,确认目标 nested entity 确实为类型。

先预测下列三个位置的结果再编译:base list、constructor initializer、function local declaration。不要只记答案,要说出 parser 在每个位置已掌握的语法证据。

小结

  • template type parameter list 中 classtypename 完全等价
  • nested dependent name 默认不按 type 解析,因为未知 specialization 可能把它定义为 value
  • 普通声明和 alias 中的 dependent qualified type 通常需要 typename
  • base-specifier list 与 base mem-initializer 的类型位置不写 typename
  • typename 只负责语法消歧,实例化仍要验证 specialization 真有该类型
  • dependent member template 的尖括号歧义由 template disambiguator 处理

资料与写作方式声明

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

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

名词解释

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

template type parameter

由具体类型实参替换的模板形参。

parameter-keyword equivalence

参数列表中 class 与 typename 等价。

dependent name
含义依赖模板参数的名称。
nested dependent name

位于 dependent scope 内部的名称。

dependent type name

预期表示类型的嵌套依赖名称。

declaration-expression ambiguity

tokens 可读成声明或表达式的歧义。

dependent-name non-type assumption

未消歧时依赖名称默认按非类型读取。

typename disambiguation

声明 qualified dependent name 是类型。

qualified-dependent-type test

判断 typename 需求的三问流程。

iterator traits dependent type

iterator_traits 提供的依赖关联类型。

nondependent known type

定义点已确定为类型的名称。

dependent alias target

alias 右侧等待实例化的类型。

base-specifier list

class declaration 中列出直接 bases 的区域。

mem-initializer list

constructor 指定 bases 和 members 初始化的区域。

context-sensitive typename rule

语法位置改变 typename 需求的规则。

deferred type validation

实例化时验证 nested type 真正存在。

template disambiguator

声明 dependent member name 是模板的关键字。

dependent-name diagnostic workflow

依赖性到 specialization 的诊断流程。

练习

  1. 问题 1:修复 iterator_traits 声明。 模板函数要创建 iterator 的 value_type 临时对象。
  1. 问题 2:解释三个位置。 Derived 继承 Base 的 Nested,在 constructor 初始化它,并在函数体创建一份 copy。
  1. 问题 3:区分 typename 与 template 消歧。 Registry 的 dependent Rebind 是 member template,其结果有 nested type。

讨论

评论区加载中…