编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
        • 1. 案例引入
          • 1.1 CRTP 神秘崩溃报告
          • 1.2 类型列表编译炸弹
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 三大武器图
          • 2.2 为何这么切
        • 3. CRTP 奇异递归模板
          • 3.1 静态多态的骨架
          • 3.2 虚函数与 CRTP 性能实测
          • 3.3 CRTP 构造期陷阱
          • 3.4 典型应用四场景
        • 4. TypeList 类型列表
          • 4.1 编译期链表的表示
          • 4.2 基本操作五件套
          • 4.3 Loki 遗产与 Boost.MPL
          • 4.4 用可变参模板重写
        • 5. 编译期算法设计
          • 5.1 编译期斐波那契三条路
          • 5.2 编译期排序与查找
          • 5.3 类型变换 pipeline
          • 5.4 编译期 switch-case 模式
        • 6. 编译期值与整型序列
          • 6.1 integral_constant 的基因级设计
          • 6.2 ratio 编译期有理数
          • 6.3 index_sequence 再审视
          • 6.4 编译期 Map 与 lookup
        • 7. mixin 与 policy 设计
          • 7.1 策略模板的范本
          • 7.2 CRTP + mixin 的叠加模式
          • 7.3 std::allocator 的 policy 基因
        • 8. 现代替代方案对比
          • 8.1 constexpr 函数替代值元函数
          • 8.2 Concepts 取代 enable_if
          • 8.3 折叠表达式替代递归 template
          • 8.4 技术选型决策树
        • 9. 编译器内幕与膨账治理
          • 9.1 模板递归与编译量
          • 9.2 实例化爆炸的四类病根
          • 9.3 五项瘦身策略
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次类型变换的全程生涯
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-05
目录

元编程模板技巧

# 23.元编程模板技巧

# 目录介绍

  • 1. 案例引入
    • 1.1 CRTP 神秘崩溃报告
    • 1.2 类型列表编译炸弹
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 三大武器图
    • 2.2 为何这么切
  • 3. CRTP 奇异递归模板
    • 3.1 静态多态的骨架
    • 3.2 虚函数与 CRTP 性能实测
    • 3.3 CRTP 构造期陷阱
    • 3.4 典型应用四场景
  • 4. TypeList 类型列表
    • 4.1 编译期链表的表示
    • 4.2 基本操作五件套
    • 4.3 Loki 遗产与 Boost.MPL
    • 4.4 用可变参模板重写
  • 5. 编译期算法设计
    • 5.1 编译期斐波那契三条路
    • 5.2 编译期排序与查找
    • 5.3 类型变换 pipeline
    • 5.4 编译期 switch-case 模式
  • 6. 编译期值与整型序列
    • 6.1 integral_constant 的基因级设计
    • 6.2 ratio 编译期有理数
    • 6.3 index_sequence 再审视
    • 6.4 编译期 Map 与 lookup
  • 7. mixin 与 policy 设计
    • 7.1 策略模板的范本
    • 7.2 CRTP + mixin 的叠加模式
    • 7.3 std::allocator 的 policy 基因
  • 8. 现代替代方案对比
    • 8.1 constexpr 函数替代值元函数
    • 8.2 Concepts 取代 enable_if
    • 8.3 折叠表达式替代递归 template
    • 8.4 技术选型决策树
  • 9. 编译器内幕与膨账治理
    • 9.1 模板递归与编译量
    • 9.2 实例化爆炸的四类病根
    • 9.3 五项瘦身策略
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次类型变换的全程生涯
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 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 处理 */ }
};
1
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
1
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 还没构造!*/ }
};
1
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;
1
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&lt;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 章
1
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 提供了现代替代方案     │
                    └──────────────────────────────┘
1
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 做行为组合——三件事看起来都是「模板」,为什么不做成一套统一框架?

论证:

  1. 操作对象不同——CRTP 操作的是运行时的对象(this),类型列表操作的是编译期的类型,policy 操作的是编译期的行为选择。三者在编译器的 AST 层面分属不同的节点类型(class template / type alias / member function template),不可能统一表示。
  2. 历史路径说明切割是必然的——1994 年 Erwin Unruh 在 C++ 标准委员会会议上用模板错误信息输出质数,这是「类型列表」的始祖(编译期计算);1995 年 Jim Coplien 发表 CRTP 经典论文,这是「静态多态」的始祖;1990 年代 policy 设计随 STL 一同标准化。三者从诞生起就沿着不同的问题域演化。
  3. 现代替代导致三块边界更清晰——C++17 的 if constexpr 把很多 CRTP「分发」场景替代为单函数内的编译期分支;C++20 的 Concepts 接管了 policy 里的「约束选择」功能;可变参模板 + 折叠表达式接管了「类型列表」的递归操作。但三者的核心理念(编译期模拟运行时行为)依然统一,只是实现手段现代化了。
  4. 反向验证:如果把三者强行统一成一个框架(如 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() { /* ... */ }
};
1
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(); }
1
2
3
4
5
6
7
8
9
call_crtp(D&):
    ret                  ; ← 空函数体(编译器看到 impl() 为空,整条链全部内联消去)

call_virtual(VBase&):
    mov   rax, [rdi]     ; 读 vptr(即使函数体为空,虚调用机制本身不消失)
    jmp   [rax]          ; 间接跳转
1
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
1
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 已初始化
};
1
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 独立计数
1
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; }
};
1
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&,可继续链式调用
1
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> 的基类
1
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>>>;
1
2
3
4
5
6
7
8
9
10
11

可视化:

my_list = int → double → char → null_type
            └──── 嵌套 ────┘
1
2

C++11 之后的现代表示——可变参模板(第 20 篇):

template <typename... Ts>
struct typelist {};
// 一行搞定,不需要 null_type,不需要递归嵌套
1
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);  // ← 编译期直接拿到
};
1
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...> {};
1
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>; };
1
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;
};
1
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> || ...);
}
1
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*>
1
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> {};
1
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&lt;Ts...> 代替 boost::mpl::vector
Step 2:元函数替换层 → 用 sizeof... / std::conditional_t / fold expression
Step 3:消除依赖层 → 最终移除 Boost.MPL 头文件
1
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);
1
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);
1
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>{});
}();
1
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 排序过了
};
1
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 借助这条信息一次分配
1
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) + ...); }
1
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,其余走序列化协议
1
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"); }
}
1
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>;
1
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 优化)
// 零开销——这个对象除了作为类型信息载体,不占任何空间
1
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>
1
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 的转换比例是准确的(没有浮点误差)
1
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) 递归)
1
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...>{});
}
1
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
1
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;
1
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
1
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 三个方法
};
1
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;
1
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;
}
1
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 {
        // 默认路径
    }
}
1
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), ...); }
1
2
3
4
5
6
7
8
9
10
11

# 8.4 技术选型决策树

你要做什么?
├─ 编译期值计算 (fib/gcd/sqrt)
│   └─ constexpr 函数 (C++14+) — 绝不用模板递归
│
├─ 类型级操作 (transform/filter/concat)
│   └─ 可变参模板 typelist&lt;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&lt;Ts...> 永远是基础
        用 std::tuple / std::variant 运行期用
        用 index_sequence 做索引和拆包
1
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 → 优化 → 汇编
  └─ 符号表更新:记录此次完全特化的位置
1
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) { /* 循环 */ }
1
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>;
1
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 && ...);
1
2
3
4
5
6

策略④:extern template 消除重复

// 在指定翻译单元里实例化,其余单元用 extern 抑制
extern template class std::vector<int>;
extern template class std::vector<double>;
// ... 每个 TU 都声明 → 链接时只保留一份实例
1
2
3
4

策略⑤:分离 TMP 代码到 .cpp

尽可能把类型列表的结果(具体类型的 typedef)而不是推导过程暴露在头文件里:

// ❌ 头文件里写一整套 transform / filter
// 每个引入这个头文件的 .cpp 都重新实例化一遍

// ✅ 头文件只暴露结果
using risk_types = typelist<PriceRisk, RateRisk, CreditRisk>;  // 只有这句
// transform/filter 过程在一个单独的 .cpp 里完成
1
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() { /* ... */ }
};
1
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 是编译期内建优化路径
1
2
3
4
5
6
7
8

编译时间:21 分钟 → 45 秒。

# 10.2 一次类型变换的全程生涯

把 auto t = transform<typelist<int,double,char>, std::add_pointer>::type 的全过程串成一棵树:

transform&lt;typelist&lt;int,double,char>, std::add_pointer>::type
       │
       ├─ 编译期:模板参数推导
       │   ├─ Ts... = (int, double, char) — 可变参解包
       │   └─ F = std::add_pointer — 元函数
       │
       ├─ 编译期:映射 (Map)
       │   ├─ F&lt;int>    = std::add_pointer&lt;int>    → int*
       │   ├─ F&lt;double> = std::add_pointer&lt;double> → double*
       │   ├─ F&lt;char>   = std::add_pointer&lt;char>   → char*
       │   └─ 每组展开是一次模板实例化(3 次)
       │
       ├─ 编译期:参数包重新包装
       │   └─ typelist&lt;int*, double*, char*> ← 新的类型列表
       │
       └─ 运行时:
           └─ 这个类型列表作为模板实参注入到用户代码
              → 代码生成、类型检查、符号表更新
              → 二进制里没有任何中间产物(只有最终生成的代码)
1
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&lt;Ts...> + std::index_sequence
└─ 消除重复                         → extern template + 显式实例化
1
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
1
2
3
4
5
6
7
8
9
10
11
12

一图定型:

模板元编程 = (CRTP + TypeList + Policy) × 编译期机器

  CRTP               TypeList             Policy
  static_cast&lt;T*>     typelist&lt;Ts...>      template&lt;Policy>
  静态多态              类型级数据            行为组合
  sizeof=0开销           transform/map         正交性
  构造期 UB 避免        现代用可变参           allocator 基因

         └──────────────┬──────────────┘
                        ▼
          编译时间爆炸治理五策略
          · constexpr 替模板递归
          · 可变参替嵌套列表
          · 折叠替递归函数
          · extern template
          · TMP 分离到 .cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

下一篇:元编程让编译器变成了另一台图灵机。下一篇进入卷三收官篇 24.Modules模块化设计——C++20 Modules 如何让「编译期图灵机」告别头文件地狱,用 import 替代 #include,把增量构建的速度提上十倍。

上次更新: 2026/06/10, 11:13:41
Concepts深度剖析
Modules模块化设计

← Concepts深度剖析 Modules模块化设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式