前言
typename是 C++ 模板里最容易被「照抄但不懂」的关键字。很多人的印象停留在「模板参数可以写typename T,也可以写class T」,于是把它当成class的同义词;一旦遇到typename T::value_type这种写法,就说不清为什么这里必须加、不加会报什么错。更麻烦的是,有些代码在某个编译器上能过,换个编译器就报「dependent name is not a type」,根源往往就是漏了或多了typename。
这个关键字的本质,是给编译器一个消歧提示。在模板定义阶段,编译器不知道T具体是什么类型,也就不知道T::value_type是一个类型、一个静态数据成员,还是一个成员函数。C++ 的语法在这种情况下默认按「非类型」解析,如果你要用它当类型,就必须用typename明确告诉编译器。C++20 起,标准在若干「只可能是类型」的位置放宽了这一要求,但依赖旧编译器或旧代码时仍会看到大量显式typename。
本文讲清三个场景:模板参数列表里的typename、依赖名前的typename、以及成员模板调用前的template关键字;最后说明 C++17 与 C++20 带来的变化。代码基准为 C++17,涉及 C++20 的地方单独标注。
一、模板参数列表:typename与class等价
在模板形参列表里,typename和class可以互换:
// 适用于 C++17
template <class T>
struct BoxA { T value; };
template <typename T>
struct BoxB { T value; };这两个声明完全等价,class在这里不代表「只能是类类型」——BoxA<int>一样合法。标准保留class只是为了兼容早期代码。新代码通常统一写typename,因为它不会让人误以为模板实参必须是类。
1.1 模板模板参数的历史差异
模板的模板参数(template template parameter)曾经有一个例外:C++17 之前只能用class,不能写typename。
// 适用于 C++17
#include <vector>
template <template <class> class Container> // C++17 之前只能这样写
struct OldStyle {
Container<int> data;
};
template <template <typename> typename Container> // C++17 起合法
struct NewStyle {
Container<int> data;
};
OldStyle<std::vector> a;
NewStyle<std::vector> b;C++17 之后两者都合法,typename版本语义更一致。维护跨标准代码时,如果最低标准是 C++14,就只能用class。
1.2 非类型模板参数不能用 typename
typename只能引入类型参数。下面这种写法是错的:
// 适用于 C++17
// template <typename int N> struct Bad; // 错误:N 是值,不是类型
template <int N>
struct Good { static constexpr int value = N; };二、依赖名:为什么这里必须写typename
理解typename的关键是两阶段查找(two-phase lookup)。编译器在解析模板定义时,先做一遍不依赖具体模板实参的查找;与T相关的名字叫依赖名(dependent name),要等到实例化时才能确定。问题在于,C++ 的语法在解析阶段必须决定T::value_type * p;到底是:
- 一条声明:
p是T::value_type类型的指针; - 还是一条乘法表达式:
T::value_type乘以p。
标准的选择是:默认按非类型解析。也就是说,编译器假设T::value_type是一个静态成员或枚举值,于是把它和p当成乘法。若你本意是声明指针,就必须写typename:
// 适用于 C++17
template <class T>
void declare_pointer() {
typename T::value_type* p = nullptr; // 声明指针
// T::value_type* q; // 被解析为乘法,通常报错
(void)p;
}这就是「typename是给编译器看的消歧符」的含义。它不改变类型,只改变解析方向。
2.1 哪些位置需要typename
只要一个依赖的限定名被当作类型使用,就需要typename。常见位置包括:
| 位置 | 示例 |
|---|
| 变量声明 | typename T::value_type x; |
| 函数返回类型 | typename T::iterator begin(); |
| 函数参数类型 | void f(typename T::value_type v); |
| 模板实参 | std::vector<typename T::value_type> v; |
| 类型别名 | using value_type = typename T::value_type; |
| 静态成员定义 | typename T::value_type T::stat = {}; |
注意「限定名」这个前提:typename T这种没有::的写法是非法的;typename int也非法。typename后面必须是一个限定名(可以带模板实参)。
2.2 基类列表与构造函数初始化列表
在类的基类列表里,依赖的限定名被语法规则隐式当作类型,因此不需要也不能写typename:
// 适用于 C++17
template <class T>
struct Derived : T::base_type { // 这里不能写 typename
Derived() : T::base_type() {} // 初始化列表同样按类型处理
};这一点常让初学者困惑:同样是T::base_type,在成员声明里要typename,在基类列表里不用。原因是语法位置已经消除了二义性——基类只能是一个类型。
2.3 别名模板与 traits
标准库的 traits 大量依赖typename。例如自己写一个取迭代器值类型的工具:
// 适用于 C++17
#include <iterator>
#include <type_traits>
template <class It>
using iter_value_t = typename std::iterator_traits<It>::value_type;
template <class It>
typename std::iterator_traits<It>::value_type
sum_all(It first, It last) {
using T = typename std::iterator_traits<It>::value_type;
T total{};
for (; first != last; ++first) {
total += *first;
}
return total;
}std::iterator_traits<It>::value_type依赖It,所以两处都需要typename。C++20 起标准库提供了std::iter_value_t,可以少写一层,但它仍然是别名模板,内部依旧依赖同样的机制。
三、template消歧关键字
与typename配套的还有template关键字,用于告诉编译器「后面这个<是模板实参列表的开始,不是小于号」。
// 适用于 C++17
template <class T>
struct Wrapper {
template <class U>
void apply() {}
};
template <class T>
void call_apply(Wrapper<T>& w) {
w.template apply<int>(); // 必须写 template
// w.apply<int>(); // 被解析为 (w.apply < int) > ()
}成员模板的调用需要w.template apply<int>();通过类型访问嵌套模板时写成T::template rebind<U>:
// 适用于 C++17
template <class Alloc, class U>
using rebind_alloc = typename Alloc::template rebind<U>::other;这里两个关键字同时出现:typename说明整个Alloc::template rebind<U>::other是类型,template说明rebind<U>是模板特化而不是比较。顺序不能颠倒。
四、C++17 与 C++20 的变化
4.1 C++17:模板模板参数可用typename
如第一节所述,C++17 起template <template <typename> typename C>合法。这是一处纯粹的语法放宽,不改变语义。
4.2 C++20:在「只可能是类型」的位置可以省略
C++20 采纳了「Down with typename」的提案,在若干语法位置允许省略typename,因为那里只可能出现类型。典型场景包括:
- 命名空间作用域的简单声明中,声明说明符序列的顶层;
- 函数定义中的返回类型;
- 类成员声明中的声明说明符序列;
new表达式中的类型;- 尾置返回类型。
// 需要 C++20
template <class T>
T::value_type f(); // 返回类型位置,可省略 typename
template <class T>
void g(T::value_type v); // 参数类型位置仍然需要 typename上面g的参数位置不在放宽范围内,所以仍要写typename。这也说明:不要凭「C++20 了就可以不写」的印象到处删typename,具体位置要查标准或编译器的支持情况。即便标准允许,团队代码风格也可能要求显式写出来,以兼容旧标准。
4.3 requires 表达式中的类型要求
C++20 的 requires 表达式里,可以用typename构成类型要求(type requirement),检查某个嵌套类型是否存在:
// 需要 C++20
template <class T>
concept HasValueType = requires {
typename T::value_type;
};这里的typename不是消歧,而是类型要求的语法组成部分。它只检查T::value_type是否是合法类型,不求值。
完整示例:一个带类型萃取的容器求和函数
下面把typename的几个用法串起来:返回类型、局部别名、迭代器、成员模板。
// 适用于 C++17
#include <iostream>
#include <list>
#include <type_traits>
#include <vector>
template <class Container>
typename Container::value_type sum(const Container& c) {
using Value = typename Container::value_type;
Value total{};
for (typename Container::const_iterator it = c.begin(); it != c.end(); ++it) {
total += *it;
}
return total;
}
template <class T>
struct Tag {
template <class U>
using Rebind = typename T::template rebind<U>::other;
};
int main() {
std::vector<int> v{1, 2, 3, 4};
std::list<double> l{1.5, 2.5};
std::cout << sum(v) << '\n';
std::cout << sum(l) << '\n';
static_assert(std::is_same<decltype(sum(v)), int>::value, "sum(v) is int");
}逐处检查:sum的返回类型是依赖名,加typename;using Value右边是依赖名,加typename;循环变量声明typename Container::const_iterator也是依赖名。Tag里的Rebind演示了typename与template的配合,不过它要求T本身提供rebind成员模板(标准分配器有,普通容器没有),这里只作为语法示例,不实例化。
常见坑点
❌ 在模板参数列表里认为class T限定 T 必须是类类型。
✅class与typename在模板形参列表中等价,int、指针、引用都可以作为实参。
❌ 函数返回类型写T::value_type f();。
✅ 依赖名当类型用时要写typename T::value_type f();。
❌ 把T::value_type * p;当成声明指针。
✅ 写成typename T::value_type* p;;不加typename会被解析成乘法表达式。
❌ 在基类列表里写struct D : typename T::base {}。
✅ 基类列表隐式按类型处理,写struct D : T::base {},加了反而可能报错。
❌ 调用依赖的成员模板时写obj.apply<int>()。
✅ 写obj.template apply<int>(),用template消歧。
❌ 写typename T::template rebind<U>::other时把两个关键字顺序弄反。
✅ 顺序是「先 typename 指出整体是类型,再 template 指出成员是模板」,即typename T::template rebind<U>::other。
❌ 以为 C++20 之后所有typename都能省。
✅ 只有标准列出的「只可能是类型」的位置能省;参数类型等位置仍需要,跨标准代码建议保留显式写法。
❌ 对非依赖名滥用typename,或写typename T(没有限定名)。
✅typename后面必须是限定名;非依赖的普通类型不需要它。
总结
| 场景 | 写法 | 说明 |
|---|
| 模板类型参数 | template <typename T> | 与class T等价 |
| 模板模板参数 | template <template <typename> typename C> | C++17 起可用 typename |
| 依赖类型 | typename T::value_type | 必须显式消歧 |
| 基类列表 | struct D : T::base | 不能写 typename |
| 依赖成员模板 | obj.template f<int>() | 用 template 消歧 |
| 嵌套模板类型 | typename T::template rebind<U>::other | 两个关键字配合 |
| requires 类型要求 | requires { typename T::type; } | 需要 C++20 |
typename不产生任何运行期代码,它解决的是编译期的语法二义性。记住一句话就够了:只要一个依赖的限定名被当作类型使用,就加typename;语法位置已经确定是类型的地方(如基类列表),不要加。至于 C++20 的省略规则,把它当成「可以少写」的便利,而不是「必须少写」的义务。
本文代码按 C++17 语义人工推演,未在本机编译;C++20 相关语法请以目标编译器和标准库的支持情况为准。