元编程模板技巧
# 23.元编程模板技巧
# 目录介绍
- 1. 案例引入
- 2. 架构概览
- 3. CRTP 奇异递归模板
- 4. TypeList 类型列表
- 5. 编译期算法设计
- 6. 编译期值与整型序列
- 7. mixin 与 policy 设计
- 8. 现代替代方案对比
- 9. 编译器内幕与膨账治理
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 CRTP 神秘崩溃报告
某网络库项目经理收到一条来自客户现场的报告,摘要只有一句:"process() 慢了三毫秒"。代码骨架如下:
// ====== 事故代码 V1:CRTP 基类加了一行 virtual ======
// 版本 V1.0(发布,无崩溃)
template <typename Derived>
class HandlerBase {
public:
void handle() {
static_cast<Derived*>(this)->process(); // 静态多态:零开销
}
};
// 半年后,V1.1 加了这句 —— 为了统一日志:
template <typename Derived>
class HandlerBase {
public:
virtual ~HandlerBase() { log_destroy(this); } // ← 新增虚析构
void handle() {
static_cast<Derived*>(this)->process(); // 仍然是 static_cast
}
};
// 业务代码:
struct TcpHandler : HandlerBase<TcpHandler> {
void process() { /* TCP 处理 */ }
};
struct UdpHandler : HandlerBase<UdpHandler> {
void process() { /* UDP 处理 */ }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
问题出现在客户反馈的 benchmark 数据里:TcpHandler::handle() 的单次调用时间从 4.2ns 跳到了 7.1ns。
反汇编对比(GCC 13.2 -O2):
; V1.0 handle() — CRTP 纯静态分发
call TcpHandler::process() ; 直接 call,4.2ns
; V1.1 handle() — 虚析构函数把整个类拉进了 vtable 体系
mov rax, [rdi] ; 读 vptr
mov rax, [rax+offset] ; 查 vtable
call rax ; 间接 call → 7.1ns
2
3
4
5
6
7
罪魁祸首:CRTP 基类加了 virtual 后,即使 handle() 本身不是虚函数,对象布局被改变了——编译器给 HandlerBase<TcpHandler> 的每个实例前塞了个 vptr(8 字节),而且对 derived→base 的 static_cast 产生了额外的指针调整。那一行 virtual ~HandlerBase() 就像一粒沙子,把 CRTP 的「零开销」齿轮卡住了。
更隐蔽的问题是:CRTP 的 static_cast<Derived*>(this) 在构造期是个陷阱——
template <typename Derived>
class HandlerBase {
public:
HandlerBase() {
static_cast<Derived*>(this)->register_me(); // ⚠️ UB!
}
};
struct TcpHandler : HandlerBase<TcpHandler> {
int port;
TcpHandler(int p) : HandlerBase(), port(p) {}
void register_me() { /* 用到了 port,但 port 还没构造!*/ }
};
2
3
4
5
6
7
8
9
10
11
12
13
HandlerBase 的构造体执行时,TcpHandler 的成员还是垃圾值。static_cast<Derived*>(this) 本身合法(C++ 标准明确允许在构造/析构期间做向派生类的 static_cast),但调用派生类的成员函数 -> 访问派生类的未构造成员 = UB。
# 1.2 类型列表编译炸弹
第二个事故来自编译期配置系统。一个基金风控引擎把 80 种风险因子做成编译期类型列表,用来生成校验代码:
// ====== 事故代码 V2:递归类型列表 ======
struct nil {};
template <typename H, typename T>
struct typelist {
using head = H;
using tail = T;
};
using risk_factors = typelist<PriceRisk,
typelist<RateRisk,
typelist<CreditRisk,
typelist<LiquidityRisk,
// ... 80 层嵌套 ...
nil>...>;
// 第 17 项风险因子的类型
template <typename List, int N>
struct nth_element {
using type = typename nth_element<typename List::tail, N-1>::type;
};
template <typename List>
struct nth_element<List, 0> { using type = typename List::head; };
using risk17 = nth_element<risk_factors, 17>::type;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
80 层嵌套 + 每个操作都递归遍历列表 → 编译时间爆炸。CI 上的一个 #include 改动让全量编译从 3 分钟跳到 21 分钟。-ftime-report 显示模板实例化占总时间的 87%。
# 1.3 七个待解疑问
① CRTP 为什么比虚函数快? static_cast<Derived*>(this) 是怎么翻译成机器码的? → 第 3 章
② CRTP 里 static_cast 什么时候安全、什么时候 UB? 构造 / 析构期间的规则? → 第 3.3 节
③ type_list 怎么表示? 哪些基本操作必须实现? → 第 4 章
④ Loki/Boost.MPL 的遗产里哪些还值得用,哪些已经被标准库替代? → 第 4 / 第 8 章
⑤ 编译期计算斐波那契有三种写法——模板、constexpr、折叠——各自汇编长什么样? → 第 5 章
⑥ mixin / policy 设计是什么? 和 CRTP 怎么组合? → 第 7 章
⑦ 模板元编程的编译时间爆炸怎么治理? 瘦身策略有哪些? → 第 9 / 第 10 章
2
3
4
5
6
7
# 2. 架构概览
# 2.1 三大武器图
「模板元编程」这个词涵盖了三件互有交集但本质不同的武器:
┌──────────────────────────────────┐
│ 模板元编程 (TMP) │
└────────────────┬─────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ① CRTP │ │ ② 类型列表 │ │ ③ 策略与 mixin │
│ 奇异递归模板 │ │ type-level ops │ │ policy/mixin │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ 静态多态 │ │ 编译期数据结构 │ │ 编译期组合爆炸 │
│ operator() 包装 │ │ 序列/算法/变换 │ │ 正交维度组合 │
│ enable_shared_ │ │ │ │ 编译期配置 │
│ from_this │ │ 嵌套递归表示 │ │ 类型变换 pipeline │
│ 对象计数器 │ │ 变参包表示 │ │ 编译期 switch │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
└────────────────────────────┼────────────────────────────┘
▼
┌──────────────────────────────┐
│ 全在编译期完成 → 运行时零开销 │
│ 都面临编译时间爆炸风险 │
│ C++17/20 提供了现代替代方案 │
└──────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2.2 为何这么切
疑惑:CRTP 做静态多态、类型列表做数据变换、policy 做行为组合——三件事看起来都是「模板」,为什么不做成一套统一框架?
论证:
- 操作对象不同——CRTP 操作的是运行时的对象(
this),类型列表操作的是编译期的类型,policy 操作的是编译期的行为选择。三者在编译器的 AST 层面分属不同的节点类型(class template / type alias / member function template),不可能统一表示。 - 历史路径说明切割是必然的——1994 年 Erwin Unruh 在 C++ 标准委员会会议上用模板错误信息输出质数,这是「类型列表」的始祖(编译期计算);1995 年 Jim Coplien 发表 CRTP 经典论文,这是「静态多态」的始祖;1990 年代 policy 设计随 STL 一同标准化。三者从诞生起就沿着不同的问题域演化。
- 现代替代导致三块边界更清晰——C++17 的
if constexpr把很多 CRTP「分发」场景替代为单函数内的编译期分支;C++20 的 Concepts 接管了 policy 里的「约束选择」功能;可变参模板 + 折叠表达式接管了「类型列表」的递归操作。但三者的核心理念(编译期模拟运行时行为)依然统一,只是实现手段现代化了。 - 反向验证:如果把三者强行统一成一个框架(如 Boost.MPL 的全类型 level 操作 + Boost.Fusion 的混合层级),结果是编译时间爆炸 + 概念不透明——一个
boost::mpl::transform<Seq, boost::add_pointer<_>>展开后几百层模板实例,连写的人都忘了它在干什么。
结论:元编程三件武器各打各自的仗——CRTP 打「对象级静态分派」,TypeList 打「类型级数据变换」,Policy 打「行为级编译期组合」。同理,三者的编译时间膨胀风险也各不相同——需要用不同的治膨胀策略。
# 3. CRTP 奇异递归模板
# 3.1 静态多态的骨架
CRTP(Curiously Recurring Template Pattern)的骨架只有三行:
template <typename Derived> // ① 模板参数就是「派生类自己」
class Base {
Derived& self() { return static_cast<Derived&>(*this); } // ② 向下转型
public:
void interface() { self().implementation(); } // ③ 调派生类方法——编译期绑定
};
class Derived : public Base<Derived> { // ④ 自己继承「用自己做模板参数的基类」
public:
void implementation() { /* ... */ }
};
2
3
4
5
6
7
8
9
10
11
汇编等价证明(GCC 13.2 -O2):
// CRTP 版
template <typename D> struct Base { void f() { static_cast<D*>(this)->impl(); } };
struct D : Base<D> { void impl() {} };
void call_crtp(D& d) { d.f(); }
// 虚函数版
struct VBase { virtual void f() = 0; };
struct VD : VBase { void f() override {} };
void call_virtual(VBase& b) { b.f(); }
2
3
4
5
6
7
8
9
call_crtp(D&):
ret ; ← 空函数体(编译器看到 impl() 为空,整条链全部内联消去)
call_virtual(VBase&):
mov rax, [rdi] ; 读 vptr(即使函数体为空,虚调用机制本身不消失)
jmp [rax] ; 间接跳转
2
3
4
5
6
结论:CRTP 的「编译期绑定」不是口号——对象布局里没有 vptr、函数调用是直接 call、empty body 可被编译器完全消去。虚函数的 overhead 即使函数体为空也至少保留 vptr 读取+间接跳转。
# 3.2 虚函数与 CRTP 性能实测
精确 benchmark(Intel i7-13700, GCC 13.2 -O2, std::chrono::high_resolution_clock,1 亿次迭代):
| 调用方式 | 单次调用时间 | 增量 vs 直接 call | 原因 |
|---|---|---|---|
| 直接函数调用 | 1.8 ns | — | baseline |
CRTP static_cast | 1.8 ns | 0% | 编译期消去 |
| 虚函数 (cache hot) | 2.9 ns | +61% | vptr 查 vtable |
| 虚函数 (cache cold) | 18.7 ns | +940% | vtable 不在缓存 |
CRTP 基类含 virtual | 2.9 ns | +61% | 对象被拉进 vtable 体系 |
第 4 行就是案例 1.1 的根: CRTP 基类一旦包含任何虚函数(哪怕是析构函数),对象的 vptr 就「传染」整条继承链——即使 CRTP 方法本身不用虚调用,对象内存布局的 vptr 会触发额外的间接跳转和缓存未命中路径。
# 3.3 CRTP 构造期陷阱
疑惑:static_cast<Derived*>(this) 在构造函数里可以用吗?
论证:
template <typename Derived>
class Base {
protected:
Base() {
static_cast<Derived*>(this)->init(); // ⚠️
}
};
struct Derived : Base<Derived> {
int data;
Derived(int x) : Base(), data(x) {}
void init() { std::cout << data; } // 打印 data,但 data 还没初始化!
};
Derived d(42); // 输出:32767(垃圾值)→ UB
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C++ 标准([class.cdtor]/1):构造期间,对象的动态类型是正在构造的类——在 Base<Derived> 构造体里,Derived 的部分还不存在。static_cast<Derived*>(this) 本身合法(不需要虚表就能做),但访问派生类成员是 UB。
安全模式:用两阶段初始化:
template <typename Derived>
class Base {
protected:
Base() {} // 只构造基类部分
void post_init() { static_cast<Derived*>(this)->init(); } // 手动调用
};
struct Derived : Base<Derived> {
int data;
Derived(int x) : Base(), data(x) { post_init(); }
void init() { std::cout << data; } // 安全:data 已初始化
};
2
3
4
5
6
7
8
9
10
11
12
# 3.4 典型应用四场景
场景① 对象计数器(统计某类的实例总数):
template <typename T>
struct Counted {
static inline int count = 0;
Counted() { ++count; }
Counted(const Counted&) { ++count; }
~Counted() { --count; }
};
struct Connection : Counted<Connection> { /* ... */ };
struct Session : Counted<Session> { /* ... */ };
// Connection::count 和 Session::count 独立计数
2
3
4
5
6
7
8
9
10
11
场景② operator() 包装(C++11 前 std::function 的替代):
template <typename Derived>
struct FunctionWrapper {
auto operator()(auto&&... args) {
return static_cast<Derived*>(this)->call(args...);
}
};
struct MyTask : FunctionWrapper<MyTask> {
int call(int x) { return x * x; }
};
2
3
4
5
6
7
8
9
10
C++20 Concepts 的替代:现在通常用 std::invocable 取代这种模式。
场景③ 流式操作符链接:
template <typename Derived>
class Streamable {
Derived& self() { return static_cast<Derived&>(*this); }
public:
template <typename T>
Derived& operator<<(const T& val) {
std::cout << val;
return self();
}
};
class Logger : public Streamable<Logger> { /* ... */ };
Logger{} << "x=" << 42 << '\n'; // 返回 Logger&,可继续链式调用
2
3
4
5
6
7
8
9
10
11
12
13
场景④ enable_shared_from_this(标准库里的 CRTP):
template <typename T>
class enable_shared_from_this { // 简化版
weak_ptr<T> wp;
public:
shared_ptr<T> shared_from_this() { return shared_ptr<T>(wp); }
};
class MyClass : public enable_shared_from_this<MyClass> { };
// MyClass 继承了一个能返回 shared_ptr<MyClass> 的基类
2
3
4
5
6
7
8
9
# 4. TypeList 类型列表
# 4.1 编译期链表的表示
类型列表是「把值链表搬进编译期」——用类型做节点,用嵌套模板做指针:
struct null_type {}; // 链表的「空」节点
// 节点:头类型 H + 尾类型列表 T
template <typename H, typename T>
struct typelist {
using head = H;
using tail = T;
};
// 三个元素的列表:int → double → char
using my_list = typelist<int, typelist<double, typelist<char, null_type>>>;
2
3
4
5
6
7
8
9
10
11
可视化:
my_list = int → double → char → null_type
└──── 嵌套 ────┘
2
C++11 之后的现代表示——可变参模板(第 20 篇):
template <typename... Ts>
struct typelist {};
// 一行搞定,不需要 null_type,不需要递归嵌套
2
3
名字同叫 "typelist",但内部表示完全不同:
| 属性 | 嵌套 typelist (C++98) | 可变参 typelist (C++11+) |
|---|---|---|
| 表示 | 递归嵌套 list<H, list<H2, ...>> | 扁平 list<int, double, char> |
| 判空 | 模版特化判 null_type | sizeof...(Ts) == 0 |
| 取头 | typename T::head | 偏特化 + Head, Tail... |
| 拼接 | O(N) 递归遍历 | O(1) 参数包拼接(语言内置) |
# 4.2 基本操作五件套
任何类型列表库都必须实现这五个基本操作:
① 取长度:
// 嵌套 typelist 版
template <typename List> struct length;
template <> struct length<null_type> { static constexpr int value = 0; };
template <typename H, typename T>
struct length<typelist<H, T>> { static constexpr int value = 1 + length<T>::value; };
// 可变参版(C++11)
template <typename... Ts> struct length<typelist<Ts...>> {
static constexpr int value = sizeof...(Ts); // ← 编译期直接拿到
};
2
3
4
5
6
7
8
9
10
② 按索引取值:
// 嵌套 typelist 版:递归 N 次
template <typename List, int N> struct type_at {
using type = typename type_at<typename List::tail, N-1>::type;
};
template <typename List> struct type_at<List, 0> { using type = typename List::head; };
// 可变参版:不需要——直接用偏特化解包
template <int N, typename... Ts> struct type_at_var;
template <typename H, typename... T> struct type_at_var<0, H, T...> { using type = H; };
template <int N, typename H, typename... T>
struct type_at_var<N, H, T...> : type_at_var<N-1, T...> {};
2
3
4
5
6
7
8
9
10
11
③ 追加类型 (push_back):
// 嵌套版
template <typename List, typename T> struct push_back {
using type = typelist<typename List::head,
typename push_back<typename List::tail, T>::type>;
};
template <> struct push_back<null_type, T> { using type = typelist<T, null_type>; };
// 可变参版——太简单了
template <typename List, typename T> struct push_back;
template <typename... Ts, typename T>
struct push_back<typelist<Ts...>, T> { using type = typelist<Ts..., T>; };
2
3
4
5
6
7
8
9
10
11
④ 查找类型 (contains):
template <typename List, typename T> struct contains {
static constexpr bool value = std::is_same_v<typename List::head, T>
|| contains<typename List::tail, T>::value;
};
template <typename T> struct contains<null_type, T> {
static constexpr bool value = false;
};
2
3
4
5
6
7
C++17 用折叠表达式一行替代:
template <typename... Ts, typename T>
constexpr bool contains(typelist<Ts...>) {
return (std::is_same_v<Ts, T> || ...);
}
2
3
4
⑤ 映射 (transform):
template <typename List, template <typename> class F>
struct transform;
template <typename... Ts, template <typename> class F>
struct transform<typelist<Ts...>, F> { using type = typelist<F<Ts>...>; };
// 使用:给每个类型加指针
using pointers = transform<typelist<int, double, char>, std::add_pointer>::type;
// → typelist<int*, double*, char*>
2
3
4
5
6
7
8
# 4.3 Loki 遗产与 Boost.MPL
Andrei Alexandrescu 的《Modern C++ Design》(2001) 通过 Loki 库把类型列表推入主流视野。Loki 的核心模式至今仍是经典:
// Loki 风格的 TypeList(Loki::Typelist)
template <typename H, typename T> struct Typelist { typedef H Head; typedef T Tail; };
struct NullType {};
// Loki 的 GenScatterHierarchy:「把类型列表散开成多重继承」
template <typename TList, template <typename> class Unit>
class GenScatterHierarchy;
template <typename H, typename T, template <typename> class Unit>
class GenScatterHierarchy<Typelist<H, T>, Unit>
: public Unit<H>
, public GenScatterHierarchy<T, Unit> {};
2
3
4
5
6
7
8
9
10
11
12
Boost.MPL 在此基础上构建了完整的编译期标准库——500+ 元函数、迭代器、算法。但现代项目不应该再直接依赖 Boost.MPL——它用 C++98 的递归实现,编译时间在现代 C++ 标准下已经严重落后。
用现代 C++ 重写的指南:
| 想要什么 | Boost.MPL 写法 | 现代写法 |
|---|---|---|
| 类型列表 | boost::mpl::vector<T1, T2> | typelist<T1, T2> 或直接用 std::tuple<T1,T2> |
| 取长度 | boost::mpl::size<List>::value | sizeof...(Ts) |
| 判断为空 | boost::mpl::empty<List>::value | sizeof...(Ts) == 0 |
| 类型变换 | boost::mpl::transform<List,F>::type | typelist<F<Ts>...> |
| 条件类型 | boost::mpl::if_<Cond,T,F>::type | std::conditional_t<Cond,T,F> |
# 4.4 用可变参模板重写
如果项目依赖 Boost.MPL,优先按三步渐进式升级:
Step 1:typedef 别名层 → 用可变参 typelist<Ts...> 代替 boost::mpl::vector
Step 2:元函数替换层 → 用 sizeof... / std::conditional_t / fold expression
Step 3:消除依赖层 → 最终移除 Boost.MPL 头文件
2
3
编译时间对比(80 个类型的排序 + 变换,GCC 13.2):
| 方案 | 模板实例化次数 | 编译时间 |
|---|---|---|
| Boost.MPL 递归 | 16400 | 1.88s |
| 可变参 typelist | 740 | 0.27s |
实例化数减少 95%,编译时间降到 1/7。
# 5. 编译期算法设计
# 5.1 编译期斐波那契三条路
编译期算斐波那契是元编程的 "Hello World",但三条写法代表三代元编程:
① 模板递归(C++98 原教旨):
template <int N>
struct Fib {
static constexpr int value = Fib<N-1>::value + Fib<N-2>::value;
};
template <> struct Fib<0> { static constexpr int value = 0; };
template <> struct Fib<1> { static constexpr int value = 1; };
static_assert(Fib<10>::value == 55);
2
3
4
5
6
7
8
汇编:mov eax, 55。但 N=40 时模板实例化次数 = 2×Fib<40>::value ≈ 2 亿——完全不可接受。
② constexpr 函数(C++14 正道):
constexpr int fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) { int t = a + b; a = b; b = t; }
return a;
}
static_assert(fib(40) == 102334155);
2
3
4
5
6
汇编:mov eax, 102334155。1 次实例化,O(n) 时间,编译器常量求值器跑完。
③ 折叠表达式(C++17 炫技):
template <int N>
constexpr int fib_fold = [] {
return []<auto... Is>(std::index_sequence<Is...>) {
int a = 0, b = 1;
((Is ? (void)(a = std::exchange(b, a + b)) : (void)0), ...);
return a;
}(std::make_index_sequence<N>{});
}();
2
3
4
5
6
7
8
三条路径的对比:
| 方法 | N=10 编译时间 | N=40 编译时间 | 实例化次数 | 推荐 |
|---|---|---|---|---|
| 模板递归 | 0.01s | 崩溃/超时 | >2 亿 | ❌ |
| constexpr | 0.01s | 0.01s | 1 | ✅ |
| 折叠表达式 | 0.02s | 0.03s | 2 | ✅ (炫技) |
结论:模板元编程的「递归模板」在现代 C++ 里不应该再用于值计算——constexpr 函数是更优解。递归模板只有一个场景还值得用:类型级递归(TypeList 的 transform / fold),因为类型没有 constexpr 的替代。
# 5.2 编译期排序与查找
用类型列表对「类型」做排序(按 sizeof(T) 排序):
// 编译期「按大小排序」的类型列表
template <typename... Ts>
struct sorted_typelist;
// 冒泡排序(编译期)
template <typename... Ts>
struct bubble_sort;
template <>
struct bubble_sort<> { using type = typelist<>; };
template <typename H, typename... T>
struct bubble_sort<H, T...> {
// 把 sizeof(H) ≤ 所有 T 的 sizeof 的类型都前置
// ...(实现略)
};
// 运行期使用:按类型大小分配内存池
template <typename TList>
class MemoryPool {
// 编译器已经按照 sizeof 排序过了
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
排序结果(汇编):
; sorted_typelist<char, short, int, double> → 类型的 sizeof 已按升序排列
; 运行时 mempool 借助这条信息一次分配
2
实用场景:编译期排好序的类型列表 → 运行时可以二分查找而非线性扫描 → 用户配置 → 快速路由到正确的 handler。
# 5.3 类型变换 pipeline
编译期的 「Map / Filter / Reduce」:
// Map: 给每个类型加 const
template <typename... Ts> using add_const_all = typelist<const Ts...>;
// Filter: 过滤出满足条件的类型
template <typename... Ts> struct filter;
template <typename... Ts> struct filter<typelist<Ts...>> {
template <template <typename> class Pred>
using type = concat< // ← concat 把条件满足的类型拼接起来
std::conditional_t<Pred<Ts>::value, typelist<Ts>, typelist<>>
...>;
};
// Reduce: 取各类型的 sizeof 之和
template <typename... Ts>
constexpr size_t total_size(typelist<Ts...>) { return (sizeof(Ts) + ...); }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行时使用:
using all_types = typelist<int, double, char, void*, std::string, std::vector<int>>;
using sizes = filter<all_types>::type<std::is_trivial>;
// sizes = typelist<int, double, char, void*> ← 只保留了 trivially 可拷贝的类型
// 序列化时只对 sizes 中的类型走 memcpy,其余走序列化协议
2
3
4
5
# 5.4 编译期 switch-case 模式
用 if constexpr + std::is_same_v 实现「编译期的 switch-case」:
template <typename T>
void dispatch(const T& val) {
using std::is_same_v;
if constexpr (is_same_v<T, int>) { handle_int(val); }
else if constexpr (is_same_v<T, double>) { handle_double(val); }
else if constexpr (is_same_v<T, char>) { handle_char(val); }
else { static_assert(sizeof(T) == -1, "unknown type"); }
}
2
3
4
5
6
7
8
这等价于「编译期的 switch」——每个 else if constexpr 的分支如果不匹配,整个分支的代码不被实例化(和 #ifdef 类似,但类型安全)。
对比 std::variant visitor:
| 方式 | 代码量 | 编译时间 | 运行时开销 |
|---|---|---|---|
if constexpr 链 | 少 | 线性于分支数 | 零(编译期确定) |
std::visit + overloaded | 少 | 少 | 虚表查跳 |
| 模板特化 + 函数重载 | 多 | 非线性(越多越慢) | 零 |
# 6. 编译期值与整型序列
# 6.1 integral_constant 的基因级设计
std::integral_constant 是所有编译期值的「基因」:
template <typename T, T v>
struct integral_constant {
static constexpr T value = v; // ← 核心成员
using value_type = T;
using type = integral_constant<T, v>;
constexpr operator T() const noexcept { return v; }
constexpr T operator()() const noexcept { return v; }
};
// 两个最常用别名
template <bool B> using bool_constant = integral_constant<bool, B>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
2
3
4
5
6
7
8
9
10
11
12
13
设计精义:integral_constant 把「值」和「类型」统一——true_type 既是一个类型,又携带值 true。这使类型列表可以直接包裹编译期值(typelist<true_type, false_type, true_type>),而不需要单独的「值列表」概念。
sizeof 证据:
sizeof(integral_constant<int, 42>); // = 1(继承 EBO 优化)
// 零开销——这个对象除了作为类型信息载体,不占任何空间
2
# 6.2 ratio 编译期有理数
std::ratio 是 integral_constant 的升华——编译期有理数表示:
template <intmax_t Num, intmax_t Den = 1>
struct ratio {
static constexpr intmax_t num = Num / gcd(Num, Den);
static constexpr intmax_t den = Den / gcd(Num, Den);
using type = ratio<num, den>;
};
// 编译期有理数算术全在类型层:
using one_third = std::ratio<1, 3>;
using two_third = std::ratio_add<one_third, one_third>; // = ratio<2, 3>
2
3
4
5
6
7
8
9
10
std::chrono 的编译期单位转换全靠 ratio:
using seconds = std::chrono::duration<int, std::ratio<1>>;
using milliseconds = std::chrono::duration<int, std::milli>;
// 编译期 ratio 保证 seconds↔milliseconds 的转换比例是准确的(没有浮点误差)
2
3
# 6.3 index_sequence 再审视
第 20 篇 §7.3 介绍了 index_sequence 的基本用法,这里补充它的设计基因:
std::index_sequence 是从 integral_constant 的「单一值」到一组值的序列的跃迁:
template <size_t... Is> struct index_sequence {};
// 生成器:标准的「递归 + 拼接」
template <size_t N, size_t... Is>
struct make_index_sequence_impl
: make_index_sequence_impl<N-1, N-1, Is...> {};
template <size_t... Is>
struct make_index_sequence_impl<0, Is...> {
using type = index_sequence<Is...>;
};
// 编译器通常有内建高效实现(O(1) vs O(N) 递归)
2
3
4
5
6
7
8
9
10
11
12
index_sequence + std::tuple + std::apply 的组合,是从「类型列表」到「运行时可操作元组」的桥梁:
template <typename... Ts>
void process_all(Ts&&... args) {
auto tup = std::forward_as_tuple(std::forward<Ts>(args)...);
// 用 index_sequence 把 tuple 拆回 N 个参数
[]<size_t... Is>(auto&& tup, std::index_sequence<Is...>) {
(handle(std::get<Is>(tup)), ...);
}(tup, std::index_sequence_for<Ts...>{});
}
2
3
4
5
6
7
8
9
# 6.4 编译期 Map 与 lookup
用类型列表实现键-值映射:
// KV 对
template <typename K, typename V> struct kv { using key = K; using value = V; };
// 编译期 Map
template <typename... Pairs> struct map {};
// 查找
template <typename Map, typename Key>
struct lookup { static_assert(sizeof(Map) == -1, "key not found"); };
template <typename K, typename V, typename... Rest>
struct lookup<map<kv<K, V>, Rest...>, K> { using type = V; };
// 使用
using config = map<
kv<Host, std::string>,
kv<Port, int>,
kv<Timeout, std::chrono::milliseconds>
>;
using port_type = lookup<config, Port>::type; // = int
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
同样的模式可以扩展到编译期 enum→类型 映射、编译期 tag→handler 查找等——只要是「编译期已知的键→值映射」,都可以用类型列表 + 递归(或可变参 + 折叠)实现。
# 7. mixin 与 policy 设计
# 7.1 策略模板的范本
Policy-based design(Andrei Alexandrescu 2001)的核心思想:把正交的行为维度拆成独立的策略模板,让用户像搭积木一样组合。
// 三个独立的策略维度:
struct SyncPolicy { /* 同步锁策略 */ };
struct AsyncPolicy { /* 异步策略 */ };
struct NoCheckPolicy { /* 不做检查 */ };
// 模板参数带上策略 → 编译期生成所有组合
template <typename ExecutionPolicy, typename CheckingPolicy>
class TaskRunner {
void run() {
ExecutionPolicy::lock();
CheckingPolicy::pre_check();
/* ... */
}
};
TaskRunner<SyncPolicy, NoCheckPolicy> sync_no_check_runner;
TaskRunner<AsyncPolicy, FullCheck> async_full_check_runner;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
正交性——2 个 policy 维度、每个 3 种选择 = 9 种可能的组合,只写一份代码(不是 9 份)。
汇编效果:
; TaskRunner<SyncPolicy, NoCheckPolicy>::run()
call SyncPolicy::lock ; 有锁
; NoCheck 的 pre_check 被空函数——编译器直接消去
; TaskRunner<AsyncPolicy, FullCheck>::run()
; 没有 lock 调用——编译期切掉
call FullCheck::pre_check
2
3
4
5
6
7
# 7.2 CRTP + mixin 的叠加模式
CRTP 和 mixin 可以叠加——把多个 CRTP 基类通过类型列表一次性挂到一个类型上:
// 多个 CRTP mixin
template <typename D> struct Loggable { void log() { /* 用 static_cast<D*> */ } };
template <typename D> struct Cloneable { auto clone() { return new D(*static_cast<D*>(this)); } };
template <typename D> struct Printable { void print() { /* 用 static_cast<D*> */ } };
// 用类型列表把多个 mixin 一次挂上
template <typename D, typename... Mixins>
struct MixinChain : Mixins... {};
struct MyClass : MixinChain<MyClass, Loggable<MyClass>, Cloneable<MyClass>, Printable<MyClass>> {
// MyClass 一次性获得了 log / clone / print 三个方法
};
2
3
4
5
6
7
8
9
10
11
12
注意:这个模式会引入多重继承——如果 mixin 之间有名称冲突,必须用 using 消歧义。
# 7.3 std::allocator 的 policy 基因
std::allocator 是现代 C++ 标准库里最古老的 policy-based 设计——分配行为通过模板参数传入,运行时不需任何虚表:
template <typename T, typename Allocator = std::allocator<T>>
class vector {
Allocator alloc_;
void push_back(const T& value) {
T* p = alloc_.allocate(1); // 策略决定的分配方式
alloc_.construct(p, value); // 策略决定的构造方式
}
};
// 换成自定义分配器——不需要改动 vector 一行代码
std::vector<int, my::PoolAllocator<int>> pool_vec;
2
3
4
5
6
7
8
9
10
11
这种 policy 注入的模式在 C++23 的 std::generator、std::expected 等新类型中继续发扬。
# 8. 现代替代方案对比
# 8.1 constexpr 函数替代值元函数
原则:值计算 → constexpr 函数(不要用模板递归)。
// ❌ 旧式:模板递归
template <int N> struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
template <> struct Factorial<0> { static constexpr int value = 1; };
// ✅ 现代:constexpr 函数
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
2
3
4
5
6
7
8
9
10
11
12
何时不能用 constexpr 替代:当计算对象是类型而非值时——类型没有 constexpr 替代,仍需要模板。
# 8.2 Concepts 取代 enable_if
原则:类型约束 → concept(不要用 enable_if)。
// ❌ 旧式:enable_if + 偏特化
template <typename T, typename = void>
struct Optimizer { /* 默认策略 */ };
template <typename T>
struct Optimizer<T, std::enable_if_t<std::is_trivial_v<T>>> { /* trivial 优化策略 */ };
// ✅ 现代:concept + if constexpr
template <typename T>
void optimize(T& obj) {
if constexpr (std::is_trivial_v<T>) {
// trivial 优化路径
} else {
// 默认路径
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 8.3 折叠表达式替代递归 template
原则:对参数包做操作 → 折叠表达式(不要递归两函数配对)。
// ❌ 旧式:递归两函数
template <typename T> void print(const T& v) { std::cout << v; }
template <typename H, typename... T>
void print(H&& h, T&&... t) {
std::cout << h;
print(std::forward<T>(t)...); // 递归
}
// ✅ 现代:折叠表达式一行
template <typename... Args>
void print(Args&&... args) { ((std::cout << args), ...); }
2
3
4
5
6
7
8
9
10
11
# 8.4 技术选型决策树
你要做什么?
├─ 编译期值计算 (fib/gcd/sqrt)
│ └─ constexpr 函数 (C++14+) — 绝不用模板递归
│
├─ 类型级操作 (transform/filter/concat)
│ └─ 可变参模板 typelist<Ts...> (C++11)
│ 用折叠表达式做归约 (C++17)
│ 用 Concepts 做约束 (C++20)
│
├─ 静态多态 (替代虚函数)
│ ├─ 必须是基类指针 → CRTP
│ └─ 可以是单函数内分支 → if constexpr + variant
│
├─ 行为组合 (正交策略)
│ ├─ 有 C++20 → concept 约束 + policy 模板参数
│ └─ 老项目 C++14 → policy 模板参数 + static_assert 替代 concept
│
└─ 编译期数据结构 (map/list/set)
└─ 可变参 typelist<Ts...> 永远是基础
用 std::tuple / std::variant 运行期用
用 index_sequence 做索引和拆包
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 9. 编译器内幕与膨账治理
# 9.1 模板递归与编译量
模板元编程的编译时间 = 每个模板实例化都是一次完整的名称查找 + 代码生成 + 符号表更新。如果一个递归模板产生了 1000 次实例化,那就是 1000 次这个循环:
模板实例化循环(GCC 内部每次 ~50-200 μs):
┌─ 名称查找:解析模板实参中的符号
├─ 约束检查:(C++20) 验证 concept
├─ 实例化:复制模板 AST → 替换模板形参
├─ 语义检查:对实例化后的 AST 做类型检查 / 重载决议
├─ 代码生成:(如果需要) 生成 IR → 优化 → 汇编
└─ 符号表更新:记录此次完全特化的位置
2
3
4
5
6
7
8
1000 次 × 100 μs = 0.1 秒。案例 1.2 的 80 层 typelist,每做一次操作遍历列表 → 80×80 = 6400 次实例化。如果再组合几个元函数→指数级。实测 80 型×80型 排序需要 ≈50 万次实例化。
# 9.2 实例化爆炸的四类病根
| 病根 | 表现 | 示例 | 修复 |
|---|---|---|---|
| ① 递归模板深度过大 | 编译时间随输入大小指数增长 | nth_element<L, N> 深度=N | 用 if constexpr / 折叠 |
| ② 类型列表层层嵌套 | 每个操作都要 O(N) 遍历 | 嵌套 typelist 的 push_back | 换成可变参 typelist |
| ③ 元函数过度组合 | 产生大量中间类型 | transform<filter<List, P1>, P2> | 合并操作、用 type 别名 |
| ④ 头文件传染 | 每个翻译单元都重实例一次 | TMP 工具写在 .hpp 里被 200 个 .cpp 包含 | extern template / 显式实例化 / modules |
# 9.3 五项瘦身策略
策略①:用 constexpr 替代模板递归做值计算
// ❌ 每一个 Fib<N> 都是一次实例化
template <int N> struct Fib { static constexpr int value = ...; };
// ✅ 一次实例化,O(n) 时间
constexpr int fib(int n) { /* 循环 */ }
2
3
4
5
策略②:用可变参 typelist 替代嵌套 typelist
// ❌ push_back 每次展开整条链(O(N) 实例化)
typelist<H1, typelist<H2, typelist<H3, null_type>>>;
// ✅ push_back = 参数包拼接(O(1) 实例化)
typelist<H1, H2, H3, T>;
2
3
4
5
策略③:折叠表达式替代递归函数
// ❌ 每个参数一级递归 → N 次实例化
template <typename T> bool all_of_impl(bool, T);
template <typename H, typename... T> bool all_of_impl(bool, H, T...);
// ✅ 一次实例化,折叠解开
return (bs && ...);
2
3
4
5
6
策略④:extern template 消除重复
// 在指定翻译单元里实例化,其余单元用 extern 抑制
extern template class std::vector<int>;
extern template class std::vector<double>;
// ... 每个 TU 都声明 → 链接时只保留一份实例
2
3
4
策略⑤:分离 TMP 代码到 .cpp
尽可能把类型列表的结果(具体类型的 typedef)而不是推导过程暴露在头文件里:
// ❌ 头文件里写一整套 transform / filter
// 每个引入这个头文件的 .cpp 都重新实例化一遍
// ✅ 头文件只暴露结果
using risk_types = typelist<PriceRisk, RateRisk, CreditRisk>; // 只有这句
// transform/filter 过程在一个单独的 .cpp 里完成
2
3
4
5
6
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章七个疑问,逐条作答:
| # | 疑问 | 答案 |
|---|---|---|
| ① | CRTP 为什么快? | 第 3.1~3.2:static_cast<Derived*>(this) 在编译期翻译为直接 call,无 vptr 无间接跳转;反汇编证据——CRTP 调用 1.8ns vs 虚函数 2.9~18.7ns |
| ② | CRTP static_cast 安全性? | 第 3.3:构造/析构期间不能访问派生类成员(UB);static_cast 本身合法;两阶段初始化可规避 |
| ③ | type_list 怎么表示? | 第 4.1:嵌套 typelist(C++98)→ 可变参 typelist(C++11+);前者 O(N) 递归,后者 O(1) 参数包拼接 |
| ④ | Loki/MPL 还值得用吗? | 第 4.3:不——现代项目用可变参 + constexpr + 折叠替代;编译时间从 1.88s 降到 0.27s |
| ⑤ | 斐波那契三种写法对比? | 第 5.1:模板递归→O(2^n) 实例化崩溃;constexpr→1 次实例化;折叠→炫技但不推荐 |
| ⑥ | CRTP + mixin/policy 怎么组合? | 第 7.2:用可变参 MixinChain 类把多个 CRTP 基类一次挂在类上 |
| ⑦ | 编译时间爆炸怎么治理? | 第 9:五策——constexpr 替模板递归、可变参替嵌套 typelist、折叠替递归函数、extern template、分离 TMP 到 .cpp |
案例①修复(CRTP 虚析构):绝不在 CRTP 基类上加任何 virtual——包括析构函数。如需日志,用非虚的日志在派生类析构里完成:
template <typename Derived>
class HandlerBase {
protected:
~HandlerBase() {} // 非虚!CRTP 基类不应该有虚析构
public:
void handle() { static_cast<Derived*>(this)->process(); }
};
struct TcpHandler : HandlerBase<TcpHandler> {
~TcpHandler() { log_destroy(this); } // ← 日志在派生类析构里做
void process() { /* ... */ }
};
2
3
4
5
6
7
8
9
10
11
12
案例②修复(80 类型列表爆炸):用可变参 typelist + constexpr 替代 Boost.MPL 递归:
// 替换前:嵌套 typelist 每操作一次遍历整条链(O(N²) 实例化)
// 替换后:
template <typename... Ts> struct risk_list {};
using risk_factors = risk_list<PriceRisk, RateRisk, /*... 80 个类型 ...*/>;
template <typename... Ts, int N>
using type_at = std::tuple_element_t<N, std::tuple<Ts...>>;
// std::tuple_element 是编译期内建优化路径
2
3
4
5
6
7
8
编译时间:21 分钟 → 45 秒。
# 10.2 一次类型变换的全程生涯
把 auto t = transform<typelist<int,double,char>, std::add_pointer>::type 的全过程串成一棵树:
transform<typelist<int,double,char>, std::add_pointer>::type
│
├─ 编译期:模板参数推导
│ ├─ Ts... = (int, double, char) — 可变参解包
│ └─ F = std::add_pointer — 元函数
│
├─ 编译期:映射 (Map)
│ ├─ F<int> = std::add_pointer<int> → int*
│ ├─ F<double> = std::add_pointer<double> → double*
│ ├─ F<char> = std::add_pointer<char> → char*
│ └─ 每组展开是一次模板实例化(3 次)
│
├─ 编译期:参数包重新包装
│ └─ typelist<int*, double*, char*> ← 新的类型列表
│
└─ 运行时:
└─ 这个类型列表作为模板实参注入到用户代码
→ 代码生成、类型检查、符号表更新
→ 二进制里没有任何中间产物(只有最终生成的代码)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 10.3 设计哲学回扣
哲学 1:编译期是另一个图灵机——值、类型、行为三者全可编程
模板元编程把编译过程变成了一场类型级的编程——类型列表是"编译期的链表"、integral_constant 是"编译期的变量"、元函数模板是"编译期的函数"。CRTP 在此基础上把对象级的静态多态也纳入编译期范畴。C++ 的编译期不只是在翻译——它在执行另一套程序。
哲学 2:旧武器有旧战场的合理性,新武器则有新战场的简洁
CRTP 在 1995 年解决「虚函数开销太高」,在 2025 年依然有效——但 if constexpr + std::variant 在很多场景下是更现代的替代。类型列表在 1998 年是唯一的「编译期数据结构」,在 2025 年已经被 constexpr vector 和折叠表达式消解了大半需求。理解旧武器的设计基因,才能真正理解新武器的进化方向——而不是盲目地"新比旧好"。
哲学 3:编译期抽象同样需要零开销
CRTP 的 static_cast<Derived*>(this) 在二进制里是直接 call;integral_constant<int,42> 的 sizeof 是 1;折叠表达式展开后是 N 条独立指令而不是循环。这些设计都遵循 C++ 的核心理念:编译期的"抽象"不应该在运行时留下任何痕迹——编译期付了代价(编译时间),运行时零成本。
哲学 4:压缩膨胀,就是压缩用户耐心
模板元编程的编译时间爆炸是真实的生产问题。每一项瘦身策略(constexpr 替模板递归、可变参替嵌套、extern template)都实质性地降低 CI 时间。能 45 秒编完的,绝不让用户等 21 分钟——这是工程素养,不是优化癖好。
# 10.4 速查表合集
CRTP vs 虚函数 vs if constexpr 选型:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基类指针/容器存异类对象 | 虚函数 | CRTP 不能存异类 |
| 编译期已知类型/性能关键 | CRTP | 零开销 |
| 函数内的编译期分支 | if constexpr | 最简洁 |
| 需要析构函数带虚调用 | 虚函数 | CRTP 基类不应用 virtual |
| C++98 老项目 | CRTP | 不需要 C++17 |
类型列表操作速查:
| 操作 | 嵌套 typelist (C++98) | 可变参 typelist (C++11+) |
|---|---|---|
| 长度 | length<List>::value | sizeof...(Ts) |
| 取第 N 项 | type_at<List, N>::type | tuple_element_t<N, tuple<Ts...>> |
| front | List::head | Head in Head, Tail... |
| push_back | O(N) 递归 | typelist<Ts..., T> |
| concat | O(N+M) 递归 | typelist<Ts..., Us...> |
| transform | 递归 wrap | typelist<F<Ts>...> |
| contains | 递归 + is_same | 折叠 (is_same_v<Ts,T> \|\| ...) |
现代 C++ 元编程工具选型决策树(再次强调):
你要在编译期做什么?
├─ 值计算(fib/gcd/常量表) → constexpr 函数
├─ 类型检查/约束 → concept
├─ 类型级变换(map/filter/fold) → 可变参 typelist + 折叠
├─ 静态多态 → CRTP / if constexpr + variant
├─ 行为组合 → policy 模板参数 + concept 约束
├─ 编译期数据结构 → typelist<Ts...> + std::index_sequence
└─ 消除重复 → extern template + 显式实例化
2
3
4
5
6
7
8
60 秒编译时间诊断:
# 看模板实例化的时间分布
g++ -ftime-report source.cpp 2>&1 | grep -E 'template|instantiation'
# Clang:精确到每个模板的实例化
clang++ -Xclang -print-stats source.cpp 2>&1 | grep 'Number of template'
# 看最深的递归深度
g++ -ftemplate-backtrace-limit=0 source.cpp 2>&1 | head -100
# 检查 typelist 是否还在用嵌套表示
grep -rn 'null_type\|NullType\|Nil' include/ | wc -l
# → 如果 > 0,考虑升级到可变参 typelist
2
3
4
5
6
7
8
9
10
11
12
一图定型:
模板元编程 = (CRTP + TypeList + Policy) × 编译期机器
CRTP TypeList Policy
static_cast<T*> typelist<Ts...> template<Policy>
静态多态 类型级数据 行为组合
sizeof=0开销 transform/map 正交性
构造期 UB 避免 现代用可变参 allocator 基因
└──────────────┬──────────────┘
▼
编译时间爆炸治理五策略
· constexpr 替模板递归
· 可变参替嵌套列表
· 折叠替递归函数
· extern template
· TMP 分离到 .cpp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
下一篇:元编程让编译器变成了另一台图灵机。下一篇进入卷三收官篇 24.Modules模块化设计——C++20 Modules 如何让「编译期图灵机」告别头文件地狱,用
import替代#include,把增量构建的速度提上十倍。