C++ Templates
模板是 C++ 的编译期多态与元编程基石。同一份模板代码,编译器根据传入的类型生成不同实例,实现“一次编写,多类型复用”,且零运行时开销。模板把“类型”当作一等公民,在编译期进行计算与分发。
类比:模板是“代码模具”,注入不同类型(材料)压出不同实例(零件)。
模板的两大用途:泛型编程(容器、算法与类型无关)与模板元编程(TMP,编译期计算)。本文聚焦模板本身的核心机制,约束部分见 [[cxx-concept]]。
函数模板与类模板
1 | |
typename 与 class 在模板参数列表中等价,习惯上泛型用 typename,需是类类型时用 class。
模板实例化
模板是蓝图,本身不产生代码。只有被使用(取地址、调用、完整类型需求)时,编译器才为具体类型生成实例(instantiation)。未使用的偏特化或成员函数不会被实例化——这是“按需实例化”的来源,也是模板错误常延迟到使用才暴露的原因。
1 | |
模板参数种类
模板参数有三类:
| 种类 | 例子 | 说明 |
|---|---|---|
| 类型参数 | template<typename T> | 最常见,T 是一个类型 |
| 非类型参数 (NTTP) | template<int N> | 常量整数/指针/引用/枚举,C++20 起可为浮点和类类型 |
| 模板参数 | template<template<typename> class C> | 参数本身是个模板 |
1 | |
全特化与偏特化
模板可对特定类型提供专门实现。
全特化
对模板的全部参数都指定具体值:
1 | |
函数模板不支持偏特化,只能重载或全特化。需要“函数偏特化”时常用类模板 + 静态方法绕过。
偏特化
只对部分参数或参数模式特化:
1 | |
偏特化匹配时,编译器选最特化的那个。
SFINAE:替换失败不是错误
SFINAE(Substitution Failure Is Not An Error)是模板的核心机制:在模板参数替换过程中,若某个候选的替换导致类型/表达式非法,该候选被静默丢弃,而非报错。这让模板能“按特性分流”。
enable_if
经典 SFINAE 工具 std::enable_if:
1 | |
enable_if<Cond>::type 在 Cond 为真时存在 type 成员,否则不存在——后者触发 SFINAE 把该重载丢弃。
常见 SFINAE 手法
1 | |
SFINAE 代码繁琐且错误信息晦涩,C++20 后优先用 Concept 替代(见 [[cxx-concept]])。SFINAE 仍需理解,因为大量既有库与 C++17 代码依赖它。
if constexpr:编译期分支
if constexpr(C++17)在编译期求值条件,丢弃不取的分支(甚至不检查语法合法性),大幅简化模板:
1 | |
对比 SFINAE,if constexpr 让“同一函数内按类型分流”变得直观,是取代部分 SFINAE 的首选。
变参模板与 Fold Expressions
参数包
C++11 变参模板用 ... 表示参数包:
1 | |
sizeof...(args) 取包大小。展开参数包是变参模板的核心。
Fold Expressions(C++17)
C++17 的 fold expression 一行展开参数包,告别递归模板:
1 | |
| 形式 | 含义 |
|---|---|
(pack op ...) | 一元右折叠 |
(... op pack) | 一元左折叠 |
(pack op ... op init) | 二元右折叠 |
(init op ... op pack) | 二元左折叠 |
C++11 递归展开
C++17 前需递归 + 终止重载:
1 | |
CTAD:类模板参数推导
C++17 起,构造对象时常可省略模板参数(Class Template Argument Deduction):
1 | |
可提供推导指引自定义推导规则:
1 | |
模板元编程(TMP)速览
模板能在编译期“计算”——把计算编码为类型与常量:
1 | |
TMP 强大但可读性差、编译慢。现代 C++ 中,编译期计算更多用 constexpr 函数替代(见 [[cxx-print]] 等文中 constexpr 用法),TMP 退居“类型计算”与“约束分发”等少数场景。
实战要点与陷阱
1. 头文件实现
模板定义通常必须放在头文件(或被使用点可见处),因为实例化需要完整定义。
2. 两阶段名称查找
模板内名称查找分两阶段:
- 定义阶段:不依赖模板参数的名称(普通函数、类型)按模板定义点查找。
- 实例化阶段:依赖参数的名称(如
T::foo、x.foo())按实例化点的实参查找(ADL)。
依赖名称需用 typename/template 消歧:
1 | |
3. 显式实例化
为减少模板代码膨胀、加速编译,可显式实例化:
1 | |
配合 extern template 声明抑制重复隐式实例化。
4. 错误信息冗长
模板错误常是“一长串替换失败堆栈”。static_assert 加自定义信息能提前截断并给出可读提示:
1 | |
C++20 Concept 提供更友好的约束与错误信息(见 [[cxx-concept]])。
现代演进:从 SFINAE 到 Concept
| 需求 | C++17 方案 | C++20 方案 |
|---|---|---|
| 约束模板参数 | enable_if | requires / Concept |
| 编译期分支 | enable_if 重载 | if constexpr / requires |
| 友好错误信息 | static_assert | Concept 天然友好 |
| 探测成员 | void_t SFINAE | requires 表达式 |
模板机制不变,但表达层正向 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]])行为的关键。







