模板是 C++ 的编译期多态元编程基石。同一份模板代码,编译器根据传入的类型生成不同实例,实现“一次编写,多类型复用”,且零运行时开销。模板把“类型”当作一等公民,在编译期进行计算与分发。

类比:模板是“代码模具”,注入不同类型(材料)压出不同实例(零件)。

模板的两大用途:泛型编程(容器、算法与类型无关)与模板元编程(TMP,编译期计算)。本文聚焦模板本身的核心机制,约束部分见 [[cxx-concept]]。

函数模板与类模板

1
2
3
4
5
6
7
8
9
10
11
// 函数模板
template<typename T>
T maxOf(T a, T b) { return a > b ? a : b; }

// 类模板
template<typename T, size_t N>
class Array {
T data[N];
public:
T& operator[](size_t i) { return data[i]; }
};

typenameclass 在模板参数列表中等价,习惯上泛型用 typename,需是类类型时用 class

模板实例化

模板是蓝图,本身不产生代码。只有被使用(取地址、调用、完整类型需求)时,编译器才为具体类型生成实例(instantiation)。未使用的偏特化或成员函数不会被实例化——这是“按需实例化”的来源,也是模板错误常延迟到使用才暴露的原因。

1
2
3
maxOf(1, 2);        // 实例化 maxOf<int>
maxOf(1.0, 2.0); // 实例化 maxOf<double>
Array<int, 4> a; // 实例化 Array<int, 4>

模板参数种类

模板参数有三类:

种类例子说明
类型参数template<typename T>最常见,T 是一个类型
非类型参数 (NTTP)template<int N>常量整数/指针/引用/枚举,C++20 起可为浮点和类类型
模板参数template<template<typename> class C>参数本身是个模板
1
2
3
4
5
template<int N>                       // NTTP
struct Buffer { char data[N]; };

template<template<typename> class Container> // 模板参数
struct Wrapper { Container<int> c; };

全特化与偏特化

模板可对特定类型提供专门实现。

全特化

对模板的全部参数都指定具体值:

1
2
3
4
5
6
7
8
template<typename T>
struct TypeName { static const char* get() { return "unknown"; } };

template<>
struct TypeName<int> { static const char* get() { return "int"; } }; // 全特化

TypeName<int>::get(); // "int"
TypeName<double>::get(); // "unknown"(用主模板)

函数模板不支持偏特化,只能重载或全特化。需要“函数偏特化”时常用类模板 + 静态方法绕过。

偏特化

只对部分参数或参数模式特化:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct TypeName<T*> { static const char* get() { return "pointer"; } }; // 指针偏特化

template<typename T, typename U>
struct Map {}; // 主模板:两参数

template<typename T>
struct Map<T, T> {}; // 偏特化:两参数相同

template<typename T>
struct Map<T, void> {}; // 偏特化:第二参数为 void

偏特化匹配时,编译器选最特化的那个。

SFINAE:替换失败不是错误

SFINAE(Substitution Failure Is Not An Error)是模板的核心机制:在模板参数替换过程中,若某个候选的替换导致类型/表达式非法,该候选被静默丢弃,而非报错。这让模板能“按特性分流”。

enable_if

经典 SFINAE 工具 std::enable_if

1
2
3
4
5
6
#include <type_traits>

// 仅当 T 是整数类型时启用
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T absT(T x) { return x < 0 ? -x : x; }

enable_if<Cond>::typeCond 为真时存在 type 成员,否则不存在——后者触发 SFINAE 把该重载丢弃。

常见 SFINAE 手法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回类型 SFINAE
template<typename T>
auto size(T& c) -> decltype(c.size()) { return c.size(); }

// 默认模板参数 SFINAE
template<typename T, typename = std::enable_if_t<std::is_class_v<T>>>
void f(T) {}

// void_t 探测成员是否存在
template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};

SFINAE 代码繁琐且错误信息晦涩,C++20 后优先用 Concept 替代(见 [[cxx-concept]])。SFINAE 仍需理解,因为大量既有库与 C++17 代码依赖它。

if constexpr:编译期分支

if constexpr(C++17)在编译期求值条件,丢弃不取的分支(甚至不检查语法合法性),大幅简化模板:

1
2
3
4
5
6
7
8
9
10
template<typename T>
void process(T x) {
if constexpr (std::is_integral_v<T>) {
std::cout << x + 1; // 整数分支
} else if constexpr (std::is_pointer_v<T>) {
std::cout << *x; // 指针分支(非指针类型不会实例化此分支)
} else {
std::cout << x; // 其他
}
}

对比 SFINAE,if constexpr 让“同一函数内按类型分流”变得直观,是取代部分 SFINAE 的首选。

变参模板与 Fold Expressions

参数包

C++11 变参模板用 ... 表示参数包

1
2
template<typename... Args>
void print(Args... args) { /* args 是参数包 */ }

sizeof...(args) 取包大小。展开参数包是变参模板的核心。

Fold Expressions(C++17)

C++17 的 fold expression 一行展开参数包,告别递归模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 一元右折叠:((a1+a2)+a3)+...
}

template<typename... Args>
void printAll(Args... args) {
((std::cout << args << ' '), ...); // 逗号折叠:依次打印
}

template<typename... Args>
bool allPositive(Args... args) {
return (true && ... && (args > 0)); // 二元左折叠,带初值
}
形式含义
(pack op ...)一元右折叠
(... op pack)一元左折叠
(pack op ... op init)二元右折叠
(init op ... op pack)二元左折叠

C++11 递归展开

C++17 前需递归 + 终止重载:

1
2
3
4
5
6
7
void print() {}   // 终止

template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first;
print(rest...); // 递归
}

CTAD:类模板参数推导

C++17 起,构造对象时常可省略模板参数(Class Template Argument Deduction):

1
2
3
std::pair p(1, 2.0);       // 推导为 std::pair<int, double>
std::vector v{1, 2, 3}; // std::vector<int>
std::lock_guard lk(mtx); // 推导锁类型

可提供推导指引自定义推导规则:

1
2
3
4
5
template<typename T>
struct Container {
Container(T val) {}
};
Container(double) -> Container<int>; // 用 double 构造时推导为 Container<int>

模板元编程(TMP)速览

模板能在编译期“计算”——把计算编码为类型与常量:

1
2
3
4
5
6
7
// 编译期阶乘
template<int N>
struct Fact { static constexpr int value = N * Fact<N-1>::value; };
template<>
struct Fact<0> { static constexpr int value = 1; };

static_assert(Fact<5>::value == 120); // 编译期求值

TMP 强大但可读性差、编译慢。现代 C++ 中,编译期计算更多用 constexpr 函数替代(见 [[cxx-print]] 等文中 constexpr 用法),TMP 退居“类型计算”与“约束分发”等少数场景。

实战要点与陷阱

1. 头文件实现

模板定义通常必须放在头文件(或被使用点可见处),因为实例化需要完整定义。

2. 两阶段名称查找

模板内名称查找分两阶段:

  • 定义阶段:不依赖模板参数的名称(普通函数、类型)按模板定义点查找。
  • 实例化阶段:依赖参数的名称(如 T::foox.foo())按实例化点的实参查找(ADL)。

依赖名称需用 typename/template 消歧:

1
2
3
4
5
template<typename T>
void f() {
typename T::iterator it; // 依赖类型,必须 typename
T::template foo<int>(); // 依赖模板,必须 template
}

3. 显式实例化

为减少模板代码膨胀、加速编译,可显式实例化:

1
2
3
// .cpp 中
template class Array<int, 4>; // 显式实例化类
template int maxOf(int, int); // 显式实例化函数

配合 extern template 声明抑制重复隐式实例化。

4. 错误信息冗长

模板错误常是“一长串替换失败堆栈”。static_assert 加自定义信息能提前截断并给出可读提示:

1
2
3
4
5
template<typename T>
void f(T x) {
static_assert(std::is_integral_v<T>, "f requires an integral type");
// ...
}

C++20 Concept 提供更友好的约束与错误信息(见 [[cxx-concept]])。

现代演进:从 SFINAE 到 Concept

需求C++17 方案C++20 方案
约束模板参数enable_ifrequires / Concept
编译期分支enable_if 重载if constexpr / requires
友好错误信息static_assertConcept 天然友好
探测成员void_t SFINAErequires 表达式

模板机制不变,但表达层正向 Concept 迁移。掌握模板核心(特化、实例化、SFINAE、变参、if constexpr)后,Concept 是更顺手的约束层。

小结

  • 模板 = 编译期蓝图,按需实例化,零运行时开销。
  • 全特化指定全部参数,偏特化只匹配部分模式(函数模板无偏特化)。
  • SFINAE 让候选按特性静默分流,enable_if/void_t 是经典工具,C++20 后优先 Concept。
  • if constexpr 简化编译期分支,丢弃不取分支。
  • 变参模板 + fold expressions 处理不定参数,告别递归。
  • CTAD 省略类模板参数,可用推导指引自定义。
  • 注意两阶段查找、头文件实现、显式实例化与错误信息治理。

模板是 [[cxx-concept]]、[[cxx-ranges]]、[[cxx-type-erasure]]、[[cxx-CRTP]] 等高级特性的共同地基,也是理解 STL(如 [[cxx-stl-allocator]])行为的关键。