Item42-- typename 的双重意义
人话版
第一件事:无关紧要的“改名”
场景: 你要定义一个模板。 代码:
template<class T> ...template<typename T> ...
人话解释: 这就好比你叫你爸,叫“父亲”还是叫“老爸”,意思完全一样,他都知道你在叫他。
- 以前 C++ 只有
class(父亲)。 - 后来觉得
typename(老爸)更贴切。 - 结论: 随便你写哪个,大家都通用
typename。
第二件事:性命攸关的“消除误会”
这是重点。为什么在模板代码里,有时候必须加 typename?
根本原因:编译器是个“直男”,思维很单一。
想象一下,你写了这么一行代码:
T::Thing * p;
你(人类)的意思是:
“喂,编译器,
T::Thing是一个类,我想定义一个指针p指向它。”
编译器(直男)看到的却是:
“哎?
T是啥我都不知道。那我猜T::Thing是个数字(静态变量),而*是乘号。所以,你是想计算T::Thing乘以p吗?”
冲突发生了! 编译器默认把所有它看不懂的 T::什么 都当成变量(数据),而不是类型。如果它当成变量,你后面的代码全都会解析错误。
解决办法: 你必须显式地告诉编译器,把它“骂”醒:
// 嘿!听好了!T::Thing 是个“类型”(typename),不是乘法运算!
typename T::Thing * p;
人话总结:
- 只要你在模板里,用到了
T肚子里面的东西(比如T::iterator)。 - 而且这个东西是个类型(不是变量)。
- 你就必须在前面加
typename,否则编译器会以为你在做算术题(乘法)。
极简口诀
- 定义模板头:
class和typename没区别,看心情用。 - 模板函数体里:如果这行代码看上去像是“乘法运算”,但你其实想定义变量,就加
typename。
详解版
1. 第一重意义:声明模板参数时
这是最简单的一层。当我们声明一个模板时,下面两种写法是完全等价的:
// 写法 A:使用 class
template<class T> class Widget;
// 写法 B:使用 typename
template<typename T> class Widget;
- 历史原因: 最早的 C++ 只有
class关键字。后来标准委员会觉得用class来修饰T有点歧义(因为T也可以是int这种非类类型),所以引入了typename。 - Scott Meyers 的建议: 既然完全一样,随你喜欢。但有些程序员习惯用
class表示T必须是类,用typename表示T可以是任何类型。但在编译器眼中,它们没区别。
2. 第二重意义:嵌套从属类型名称 (The Real Nightmare)
这才是本条款的核心。
什么是“嵌套从属类型名称”?
这句话听起来很拗口,我们拆开看:
- 从属 (Dependent): 这个名称依赖于模板参数(比如
T)。 - 嵌套 (Nested): 这个名称在那个依赖对象的内部(比如
T::Something)。 - 类型名称 (Type Name): 这个名称指的是一种数据类型,而不是变量或函数。
遇到的问题:编译器的困惑
看下面这段代码:
template<typename C>
void print2nd(const C& container) {
if (container.size() >= 2) {
// 我们的意图:定义一个迭代器 iter,指向容器的第二个元素
C::const_iterator iter(container.begin()); // ❌ 潜在的编译错误
// ...
}
}
为什么会出错? 编译器在处理模板时,当它看到 C::const_iterator,它不知道 C 是什么(因为 C 还没被实例化)。
在 C++ 的解析规则中,编译器有一个默认假设:“如果我不知道它是什么,那它就不是类型,而是个变量(或值)。”
想象一下,如果有人写了这样一个奇怪的类:
class Weird {
public:
static int const_iterator; // const_iterator 是个静态整数变量!
};
那么 C::const_iterator iter 就变成了 Weird::const_iterator 乘以 iter(乘法运算)。
为了避免这种歧义,编译器默认把 C::const_iterator 当作一个变量名,而不是类型名。所以当你试图用它定义变量 iter 时,编译器会报错:“你在用一个变量来声明另一个变量吗?”
解决方案:强制告诉编译器“这是个类型”
这就是 typename 的第二重意义:用来修饰“嵌套从属类型名称”。
我们需要显式地加上 typename:
template<typename C>
void print2nd(const C& container) {
if (container.size() >= 2) {
// ✅ 告诉编译器:C::const_iterator 是个类型!
typename C::const_iterator iter(container.begin());
// ...
}
}
规则总结:
任何时候,当你想要在模板中指代一个依赖于模板参数的内部类型时,你必须在前面加上
typename。
3. 特例:不需要(也不能)加 typename 的地方
虽然规则说“必须加”,但有两个特殊的上下文是例外的。在这些位置,编译器能确定它一定是类型,所以禁止加 typename。
1. 基类列表 (Base Class Lists) 2. 成员初始化列表 (Member Initialization Lists)
template<typename T>
class Derived : public Base<T>::Nested { // ❌ 这里不能加 typename
public:
explicit Derived(int x)
: Base<T>::Nested(x) // ❌ 这里不能加 typename
{
// ✅ 这里是函数体内部,必须加 typename
typename Base<T>::Nested temp;
}
};
4. 常见的实际应用:Traits 和 Typedef
在实际写代码(特别是写库)时,为了避免每次都写长长的 typename C::const_iterator,我们通常会结合 typedef(或 C++11 的 using)来使用。
C++98 风格:
template<typename C>
void work(const C& container) {
typedef typename C::value_type V_Type; // 获取容器存储的元素类型
V_Type temp_var;
// ...
}
C++11/Modern 风格 (推荐):
template<typename C>
void work(const C& container) {
using V_Type = typename C::value_type; // 更加清晰
V_Type temp_var;
// ...
}
5. C++20 的更新(补充知识)
值得一提的是,C++20 放宽了这一限制。在一些编译器能够明确推断出它是类型的上下文中(例如作为函数返回类型),你可以省略 typename。
// C++20 之前:必须加 typename
template<typename T>
typename T::value_type get_value(const T& c);
// C++20:某些情况下可以省略(但为了兼容性,大部分人还是会写)
template<typename T>
T::value_type get_value(const T& c);
但在函数体内部(如声明变量时),这条规则依然存在,且非常重要。
总结 (Takeaway)
- 声明模板参数时:
class和typename是一样的,随便用。 - 在模板内部时:如果你要引用一个依赖于模板参数的类型(例如
T::SubType),你必须在前面加上typename。- 例如:
typename T::iterator * iter;(声明指针)。 - 如果不加,编译器会以为你在做乘法(
T::iterator乘以iter)。
- 例如:
- 例外:在继承列表和构造函数初始化列表中,不要加
typename。
浙公网安备 33010602011771号