C++模板元编程-Cpp98版
C++98 模板元编程精讲(C++98/C++03)
面向读者:已经会写 C++ 函数和类、会用模板写泛型代码(如
template<typename T> T max(T a, T b)),但不知道"模板元编程"(Template Metaprogramming, TMP)是什么。本笔记用 C++98/C++03 标准(最原始的模板能力)讲解模板元编程。目的是让你看明白:那些 2000~2010 年代的老代码、Boost 库里的花哨写法,到底在干什么;同时为理解 C++20 的 constexpr/concepts 打地基。
全程"先讲原理、再配可编译的实际例子",每段代码都能放进一个
.cpp里跑。
第0部分 一张图看懂模板元编程
普通程序: 数据在运行时变化 → 函数/对象,运行时计算
模板元编程: 类型和常量在"编译期"变化 → 模板实例化,编译期计算
核心思想一句话:模板不只是"自动生成多个版本函数"的工具,它还是一种编译期执行的编程语言——代码写在模板里,由编译器在编译时"运行"。
这门"语言"的能力:
- 编译期计算整数(阶乘、斐波那契、质数判断……);
- 编译期操作类型(给类型加 const、去掉指针、判断是否同类……);
- 编译期做控制流(用模板特化模拟 if/switch,用递归模拟循环)。
为什么 2026 年还要学它?
- 大量现存代码库(Boost、旧项目、游戏引擎)是 C++98 时代写的,你要能读懂;
- C++11 之后的
constexpr能替代大部分"算数值"的元编程,但操作类型这件事至今只能靠模板——这是 TMP 至今不过时的核心价值; - 理解了 TMP,你才能真正看懂
std::enable_if、std::is_same、concepts 等现代工具在解决什么。
第1部分 准备知识:模板是怎么"实例化"的
在进入元编程前,先把普通模板的两个机制搞透——因为元编程全靠它们。
1.1 函数模板与类模板的实例化
函数模板(一个"函数工厂"):
// 这不是一个函数,是"造函数"的模具
template<typename T>
T my_max(T a, T b) {
return a < b ? b : a;
}
调用 my_max(3, 5) 时,编译器用 T=int 实例化出一个真实函数。调用 my_max(3.0, 5.0) 再实例化出一个 double 版本。实例化发生在编译期。
类模板(一个"类工厂"):
template<typename T>
struct Box {
T value;
Box(const T& v) : value(v) {}
T get() const { return value; }
};
Box<int>、Box<std::string> 是两个完全不同的类型,各自独立实例化。
1.2 模板实参不一定是类型
模板参数除了 typename T(类型),还可以是非类型参数(整数、指针、引用):
template<int N> // 非类型参数:编译期常量
struct Array {
int data[N]; // N 必须是编译期常量
};
template<unsigned W, unsigned H> // 多个非类型参数
struct Screen { /* ... */ };
Array<10>、Array<100> 是不同类型。注意:N 是编译期常量,不能是运行时变量。 这是元编程最重要的原料——我们用它把"数字"放进类型系统。
类模板也能有多个类型参数 + 非类型参数混用:
template<typename T, int Size>
struct Buffer { T mem[Size]; };
1.3 什么是"模板参数推导"
调用函数模板时,编译器能从实参推导出模板参数(T 由参数类型自动得出);而类模板必须显式写出(C++17 之前),如 Box<int>。
推导:编译器"猜"出模板参数 (函数模板)
指定:你直接给出模板参数 (类模板)
第2部分 模板元编程的三大武器
C++98 的模板元编程只有三件武器,全部工具都由它们组合而成:
- 模板特化/偏特化(做"编译期 if"和"分支")
- 模板递归实例化(做"编译期循环")
- typedef / 静态常量成员(做"编译期变量赋值")
2.1 武器一:模板特化(全特化)—— 编译期的 if
全特化:为某个具体类型专门写一份实现,编译器遇到该类型就用特化版,否则用通用版。这就是"编译期 if (类型==特化类型)"。
template<typename T>
struct Traits {
static const char* name() { return "unknown"; }
};
template<>
struct Traits<int> { // 全特化 int 版本
static const char* name() { return "int"; }
};
template<>
struct Traits<double> { // 全特化 double 版本
static const char* name() { return "double"; }
};
// 使用:编译期就"知道"该选哪份
Traits<int>::name(); // → "int"
Traits<float>::name(); // → "unknown"(走通用版)
把特化理解成"编译期 switch":编译器按 T 在编译期选择分支,运行期完全没判断、没开销。
2.2 武器二:模板递归实例化 —— 编译期的循环
把递归模板套在自己身上,让编译器一层层生成实例,直到特化版"兜底"终止。这就是元编程的"循环"。
template<unsigned N>
struct Factorial {
static const unsigned value = N * Factorial<N-1>::value; // 递归引用自己
};
template<>
struct Factorial<0> { // 特化版:递归出口
static const unsigned value = 1;
};
Factorial<5>::value 的计算过程:
Factorial<5>::value = 5 * Factorial<4>::value
= 5 * 4 * Factorial<3>::value
= 5 * 4 * 3 * Factorial<2>::value
= 5 * 4 * 3 * 2 * Factorial<1>::value
= 5 * 4 * 3 * 2 * 1 * Factorial<0>::value
= 5 * 4 * 3 * 2 * 1 * 1 ← 特化版终止
= 120
注意:编译器会真的生成 Factorial<5>、Factorial<4>…Factorial<0> 共 6 个不同的类(类型)。这是在编译期展开的,运行期只用一个整数 120。
两个必背要点:
- 递归必须有特化版出口(否则编译器递归到死、报
recursive template instantiation exceeded maximum depth); - 递归的每一层都是一个独立的类型,多一层就多一份编译负担——这也是为什么元编程会拖慢编译。
2.3 武器三:typedef 和常量成员 —— 编译期的"变量"
元编程里没有"变量赋值",只有两种"存结果"的方式:
- 存"值":
static const静态常量成员(或 enum); - 存"类型":
typedef类型别名(C++98 只能用 typedef,没有 using)。
template<typename T>
struct TypeInfo {
typedef T Original; // 把 T 原样保存
static const int is_pointer = 0; // 存一个编译期常量
};
template<typename T>
struct TypeInfo<T*> { // 偏特化:T 是指针
typedef T Original; // 去掉一层指针保存
static const int is_pointer = 1;
};
到这里,三大武器齐了。下面用它们组合出真正的程序。
第3部分 编译期整数计算(元编程的"Hello World")
3.1 阶乘(经典中的经典)
#include <iostream>
template<unsigned N>
struct Factorial {
static const unsigned value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> {
static const unsigned value = 1;
};
int main() {
std::cout << Factorial<5>::value << "\n"; // 120,编译期就算好
std::cout << Factorial<10>::value << "\n"; // 3628800
return 0;
}
问题:static const unsigned 在 C++98 里取地址/做引用时需要在类外定义(template<unsigned N> const unsigned Factorial<N>::value;)。为避免麻烦,惯用法是用 enum:
template<unsigned N>
struct Factorial {
enum { value = N * Factorial<N-1>::value }; // enum 是编译期常量,无需类外定义
};
template<>
struct Factorial<0> {
enum { value = 1 };
};
3.2 斐波那契(双递归)
template<unsigned N>
struct Fibonacci {
enum { value = Fibonacci<N-1>::value + Fibonacci<N-2>::value };
};
template<>
struct Fibonacci<0> { enum { value = 0 }; };
template<>
struct Fibonacci<1> { enum { value = 1 }; };
Fibonacci<10>::value → 55。教训:因为每一层都生成独立类型,双递归会导致实例数量爆炸(指数级),编译时间急剧上升。元编程"复杂度"体现在编译时长上,而不在运行时长上。
3.3 质数判断(把"函数"改写成模板)
把普通函数翻译成模板,是练习 TMP 的好方法。普通版:
bool is_prime(unsigned n) {
if (n < 2) return false;
for (unsigned d = 2; d * d <= n; ++d)
if (n % d == 0) return false;
return true;
}
模板版(用"递归 + 特化"模拟循环 + if):
// 判断 n 是否被 [2, d] 中任意数整除
template<unsigned n, unsigned d>
struct IsDivisible {
enum { value = (n % d == 0) || IsDivisible<n, d-1>::value };
};
// 出口:d 降到 1,说明没被整除
template<unsigned n>
struct IsDivisible<n, 1> {
enum { value = false };
};
template<unsigned n>
struct IsPrime {
enum { value = !IsDivisible<n, n-1>::value };
};
template<>
struct IsPrime<0> { enum { value = false }; };
template<>
struct IsPrime<1> { enum { value = false }; };
template<>
struct IsPrime<2> { enum { value = true }; };
测试:IsPrime<7>::value → true,IsPrime<8>::value → false。
这里有个元编程经典陷阱:(n % d == 0) || IsDivisible<n, d-1>::value 用了运行期的 || 短路。问题是:模板实例化是编译期的,编译器会先实例化 IsDivisible<n,d-1>(无论运行期短路与否),所以 IsDivisible<5,4> 一定会实例化 IsDivisible<5,3>…一直到底。"短路"在编译期不生效——所有分支都会实例化! 这就是为什么必须有特化出口。
对比 C++20:到了 C++17 的
if constexpr,编译器真的会跳过未选择的分支,不再实例化——这正是它替代大量 TMP 技巧的原因(详见 C++20 篇)。
第4部分 编译期类型运算(TMP 的核心价值)
算数字是"开胃菜",操作类型才是 TMP 至今不可替代的主菜。原理很简单:类模板的参数可以是类型,所以类型也能像值一样被"递归、分支、传递"。
4.1 去除一层指针:remove_pointer
template<typename T>
struct RemovePointer {
typedef T Result; // 通用:T 不是指针,原样返回
};
template<typename T>
struct RemovePointer<T*> { // 偏特化:T* 匹配这里
typedef T Result; // 剥掉一层指针
};
RemovePointer<int*>::Result == int
RemovePointer<int**>::Result == int*
RemovePointer<int>::Result == int
偏特化(T*)和全特化(int)的区别:全特化是"具体到某个类型",偏特化是"匹配一类模式"。T* 匹配任意指针类型——这是类型运算的主力。
4.2 判断类型是否相同:is_same(自己实现一遍)
标准库 std::is_same(C++11 才有)的底层就是这样:
template<typename T, typename U>
struct IsSame {
enum { value = false };
};
template<typename T>
struct IsSame<T, T> { // 两参数相同 → 匹配这个偏特化
enum { value = true };
};
IsSame<int, int>::value == true
IsSame<int, long>::value == false
理解要点:IsSame<int,int> 与 IsSame<int,long> 走的是不同模板(前者命中偏特化,后者用通用版),这是编译器在编译期完成的"类型比较"。
4.3 给类型加 const:add_const
template<typename T>
struct AddConst {
typedef const T Result;
};
// 无需特化——const T 就是结果
4.4 判断是否是指针:is_pointer + 去掉引用(约束版本省略)
template<typename T>
struct IsPointer {
enum { value = false };
};
template<typename T>
struct IsPointer<T*> {
enum { value = true };
};
4.5 条件类型选择(编译期三元表达式)
C++98 没有 std::conditional,但可以自己写(这是 Boost/TR1 conditional 的雏形):
template<bool Cond, typename TrueT, typename FalseT>
struct Conditional {
typedef TrueT Result;
};
template<typename TrueT, typename FalseT>
struct Conditional<false, TrueT, FalseT> {
typedef FalseT Result;
};
typedef Conditional<IsPointer<int*>::value, int, void>::Result X; // X == int
typedef Conditional<IsPointer<float>::value, int, void>::Result Y; // Y == void
这里演示了 TMP 最经典的结构:第一个模板参数是 bool(编译期条件),用偏特化 <false, ...> 做分支。整个 "if-else" 在编译期完成。
为什么不用继承/运行时 if? 因为这些选择必须在编译期决定——
Conditional<...>::Result是一个类型,它要作为另一个模板的参数(如std::vector<Result>),运行时再判断就来不及了。
第5部分 编译期控制流:循环、分支、开关
现在把第2部分的武器组合成标准套路。
5.1 编译期循环:把运行时 for 翻译成模板
目标:运行时求 1+2+...+N:
int sum1(int n) { int s = 0; for (int i = 1; i <= n; ++i) s += i; return s; }
模板版:
template<int N>
struct Sum {
enum { value = N + Sum<N-1>::value }; // "i += " → 递归累加
};
template<>
struct Sum<0> {
enum { value = 0 };
};
Sum<100>::value == 5050,编译期算好,运行期零开销。
5.2 编译期分支链:多层特化 = switch-case
template<int N>
struct Rank { static const char* name() { return "?"; } };
template<> struct Rank<0> { static const char* name() { return "zero"; } };
template<> struct Rank<1> { static const char* name() { return "one"; } };
template<> struct Rank<2> { static const char* name() { return "two"; } };
运行时只能写 if/switch 从一堆分支里挑;编译期直接"引用 Rank<1>::name() 就是那个特化版本"。
5.3 编译期逻辑:AND / OR / NOT
用特化实现逻辑运算(注意:运行时 && 的短路在编译期无效,必须靠特化"真"做到短路):
template<bool A, bool B>
struct And {
enum { value = false };
};
template<>
struct And<true, true> {
enum { value = true };
};
事实上 C++98 里 And<A,B>::value 也可以直接写 A && B(编译期布尔表达式是合法的模板实参),但偏特化版本能真正跳过未用分支,是更"元编程"的写法。简单判断用 && 就够了。
5.4 控制实例化深度:给递归加"墙"
递归元编程最常见的坑是编译器报:
error: template instantiation depth exceeds maximum of 1024
为什么:没有特化出口,或出口写错,编译器一路递归到深度上限。解决:必须保证每一层都单调减小,且最终命中特化。这也是为什么 Factorial<N> 的出口写 Factorial<0> 而不是 Factorial<N-1>=0(模板匹配靠模式,不靠"计算")。
第6部分 实战:编译期展开循环(loop unrolling)
元编程最早的实际应用之一是"手动循环展开",用模板在编译期把循环体复制 N 份,省掉运行期的分支判断。这对数值计算(矩阵乘法、点积)意义重大。
6.1 编译期点积(两个定长数组的内积)
template<int N>
struct DotProduct {
static int eval(const int* a, const int* b) {
return a[N-1] * b[N-1] + DotProduct<N-1>::eval(a, b); // 展开一层 + 递归
}
};
template<>
struct DotProduct<0> {
static int eval(const int* a, const int* b) { return 0; } // 出口
};
调用:
int a[4] = {1, 2, 3, 4};
int b[4] = {5, 6, 7, 8};
int result = DotProduct<4>::eval(a, b); // 1*5+2*6+3*7+4*8 = 70
DotProduct<4>::eval 被展开成 a[3]*b[3] + (a[2]*b[2] + (a[1]*b[1] + (a[0]*b[0] + 0)))——没有循环变量、没有运行时判断,一串乘法指令连在一起。
6.2 循环展开的实际意义
- 老编译器优化不强时,手动展开能显著提速(现代编译器大多会自动做,但理解这个机制仍然重要);
- 它演示了 TMP 的关键心智模型:"把循环变量从运行期参数变成模板参数,循环就在编译期展开"。
注意:
DotProduct<N>::eval是函数模板的成员函数,只在被调用时才实例化,所以递归到 0 必须保证调用链确实在编译期能终止。
第7部分 实战:类型列表(Typelist)
TMP 的核心数据结构是类型列表——一个用"类型嵌套类型"拼成的链表。它是 Boost.MPL、Loki 库的基石。
7.1 定义 Typelist
struct NullType {}; // 空表哨兵
template<typename Head, typename Tail>
struct Typelist {
typedef Head First; // 表头类型
typedef Tail Rest; // 表尾(剩余部分)
};
拼表方式(本质是"右折叠"):
// 定义一个包含 int、double、char 的类型列表
typedef Typelist<int, Typelist<double, Typelist<char, NullType> > > MyList;
// ^Head ^Tail = 另一个 Typelist
// 注意 C++98 里 > > 之间必须有空格(否则被解析成右移运算符)
7.2 取第 K 个类型(模板递归遍历表)
template<typename List, unsigned Index>
struct At {
typedef typename List::Rest NextList; // 表头不是目标,跳到表尾
typedef typename At<NextList, Index-1>::Result Result;
};
template<typename List>
struct At<List, 0> { // 目标在表头
typedef typename List::First Result;
};
// 使用:
At<MyList, 0>::Result == int
At<MyList, 1>::Result == double
At<MyList, 2>::Result == char
C++98 关键语法 typename:List::Rest 是依赖型名称(依赖模板参数 List),编译器不知道 Rest 是类型还是成员变量,必须加 typename 告诉它"这是个类型"。漏掉 typename 是 C++98 最常见的编译错误之一。
7.3 统计长度
template<typename List>
struct Length;
template<>
struct Length<NullType> {
enum { value = 0 };
};
template<typename Head, typename Tail>
struct Length<Typelist<Head, Tail> > { // 偏特化匹配"非空列表"
enum { value = 1 + Length<Tail>::value };
};
Length<MyList>::value == 3。
7.4 Typelist 的实际用途
- 编译期生成类型表:比如一个"类型 → 处理函数"的分发表,运行时查表调用;
- 实现
std::variant/boost::variant:多类型合一的容器; - 自动生成代码:对类型列表逐个特化,生成成批的同类代码。
第8部分 杀手锏:SFINAE(替换失败不是错误)
C++98 元编程最有威力的机制叫 SFINAE——Substitution Failure Is Not An Error(替换失败不是错误)。
8.1 什么是 SFINAE
当编译器给模板替换实参时,如果某个替换导致非法(比如 T::type 不存在),这个模板不会报错,而是从候选里悄悄消失,去试下一个重载。只有所有候选都失败才报错。
运行时和编译期的类比:
// 普通函数重载(运行期/编译期都适用)
void f(int) { }
void f(double) { }
f(3); // 选 int 版本
// 模板重载 + SFINAE:编译器"试"多个候选,失败的自动淘汰
8.2 经典 SFINAE 实现"类型是否可转化"
目标:判断 T 是否能用 double 构造/转化。用"替换失败即淘汰"做探测:
template<typename T>
struct CanConvertToDouble {
// 一个 0 长度数组。替换成功 → 类型存在 → 有成员 value=1
template<typename U>
static char test(U(*)[sizeof(static_cast<double>(U()))] = 0); // 注意:C++98 写法
// 通用 fallback:返回值类型不同 → 形成重载
template<typename U>
static char (&test(...))[2];
enum { value = sizeof(test<T>(0)) == 1 };
};
逐行拆解:
- 两个
test是重载:第一个要求U能转成double(替换成功才算数),第二个是万能兜底...(接受一切); - 编译器对
test<T>(0)做重载决议:能转 double 就选第一个(返回 1 字节数组),否则退而求其次选兜底(返回 2 字节数组); sizeof在编译期拿到结果 → 用==1得到bool。
虽然这段代码偏"奇技淫巧",但这正是 std::is_convertible 等 traits 在 C++98 时代的真实实现原理。现代 C++ 用 decltype + void_t 简化了它,但思想未变。
8.3 SFINAE 的简化写法(日常够用)
更常用的 SFINAE 用法是"条件性启用"某个函数。C++98 没有 std::enable_if,但可以自己写一个:
// 自制的 enable_if(C++98 版)
template<bool, typename T = void>
struct EnableIf { typedef T type; };
template<typename T>
struct EnableIf<false, T> { /* 没有 type!*/ };
// 用法:给函数返回值做"门槛"
template<typename T>
typename EnableIf<IsPointer<T>::value, int>::type // 只有 T 是指针时,这个函数才存在
get_size(const T& p) {
return sizeof(*p);
}
T是指针 →EnableIf<true,int>::type=int,函数存在;T不是指针 →EnableIf<false,int>没有 type 成员 → 替换失败 → SFINAE 把这个函数淘汰。
这是 C++98 里实现"编译期条件函数重载"的标准姿势,std::enable_if(C++11)就是它的正式版。用它可以根据类型特征让同一名字的模板按需启用不同版本。
第9部分 综合实战:一个完整的 C++98 TMP 应用
把前面所有技术拼起来:写一个编译期的"类型→字符串描述"工具(脱胎于实际代码里常见的 type traits + 分发表)。
9.1 目标
给任意类型打印编译期可得的描述信息:类型名、是否指针、能否转 double、指针解引用后的类型。
9.2 完整代码(C++98 可编译)
#include <iostream>
// ---- 1. 类型特征:is_pointer ----
template<typename T> struct IsPointer { enum { value = false }; };
template<typename T> struct IsPointer<T*> { enum { value = true }; };
// ---- 2. 去掉指针:remove_pointer ----
template<typename T> struct RemovePointer { typedef T type; };
template<typename T> struct RemovePointer<T*> { typedef T type; };
// ---- 3. 条件选择类型:conditional ----
template<bool C, typename A, typename B> struct Cond { typedef A type; };
template<typename A, typename B> struct Cond<false, A, B> { typedef B type; };
// ---- 4. 类型名:用特化做分发表 ----
template<typename T> struct TypeName { static const char* get() { return "unknown"; } };
template<> struct TypeName<int> { static const char* get() { return "int"; } };
template<> struct TypeName<double> { static const char* get() { return "double"; } };
template<> struct TypeName<char> { static const char* get() { return "char"; } };
// ---- 5. 组合:打印类型信息 ----
template<typename T>
void describe() {
std::cout << "type = " << TypeName<T>::get()
<< ", is_pointer = " << (IsPointer<T>::value ? "yes" : "no")
<< ", underlying = "
<< TypeName<typename RemovePointer<T>::type>::get()
<< "\n";
}
int main() {
describe<int>(); // type = int, is_pointer = no, underlying = int
describe<double*>(); // type = unknown, is_pointer = yes, underlying = double
describe<char**>(); // is_pointer = yes, underlying = char*(去一层)
return 0;
}
运行输出:
type = int, is_pointer = no, underlying = int
type = unknown, is_pointer = yes, underlying = double
type = unknown, is_pointer = yes, underlying = char*
这个例子的意义:describe<T>() 在编译期完成了"取特征 → 选分支 → 剥类型 → 查名字"整条链路,运行期只是打印。这就是真实 TMP 代码的骨架。
第10部分 实际工程中遇到的 C++98 TMP(读代码能力)
你不需要天天手写 TMP,但要能读懂以下老代码。列几个真实场景:
10.1 Boost 库的 type_traits
#include <boost/type_traits.hpp>
// 用法类似我们上面自制的 traits
boost::is_pointer<T>::value
boost::remove_pointer<T>::type
Boost.TR1 / Boost.TypeTraits 就是 C++98 时代 traits 的标准实现,现在全进了 std::。
10.2 Loki 库的 Typelist 与泛型组件
Loki(Modern C++ Design 配套库)大量使用 Typelist、模板递归、SFINAE 实现"编译期策略选择",是 TMP 的"教科书实现"。
10.3 老项目里的"编译期断言"
C++98 没有 static_assert(C++11 才有),老代码常这样模拟:
// 编译期断言:利用"数组长度为负"编译不过
template<bool> struct StaticAssert;
template<> struct StaticAssert<true> {};
// 用法:
StaticAssert<sizeof(int) == 4> assert_int_is_4bytes;
// 若 sizeof(int)!=4,StaticAssert<false> 未定义 → 编译错误
作用:编译期检查类型大小、平台假设,一旦违反直接编译失败——比运行时 assert 更早发现问题。
10.4 模板参数作为"编译期配置开关"
游戏/嵌入式老代码常用 template<int EnableX> 在编译期开关功能,被关掉的分支根本不生成代码:
template<bool UseLog>
struct Logger {
static void log(const char* msg) { /* 空实现,什么都不做 */ }
};
template<>
struct Logger<true> {
static void log(const char* msg) { std::cout << msg << "\n"; }
};
第11部分 C++98 TMP 的局限(为什么要等 C++20)
学完这些,你可能会问:这么麻烦,值吗?C++98 TMP 有四个硬伤,正是它们催生了现代 C++ 的特性:
- 语法丑陋:
typename、::type、enum{value}、>>之间要加空格,可读性差; - 没有 if constexpr:条件分支全靠特化/偏特化,"编译期短路"不存在,所有分支都实例化(慢);
- 算数值只能用整型:算
Factorial、Sum勉强,算浮点/字符串/复杂表达式非常痛苦; - 调试地狱:编译错误信息堆叠成百行模板递归,人很难定位。
出路:C++11 引入 constexpr(编译期算数值)、C++17 引入 if constexpr(真正的编译期分支)、C++20 引入 concepts(约束模板)。它们替代了 TMP 的"算值"和"控制流"部分,但"操作类型"仍然只能靠模板——所以理解本笔记的原理,是理解所有现代模板机制的基础。
总结:C++98 TMP 一张表
| 需求 | C++98 技术 | 现代替代(C++11/17/20) |
|---|---|---|
| 编译期算整数 | 递归模板 + 特化出口 | constexpr 函数 |
| 编译期分支 | 全特化 / 偏特化 / 枚举 | if constexpr(C++17) |
| 编译期逻辑 | 特化实现 And/Or | constexpr bool |
| 类型比较 | IsSame 偏特化 |
std::is_same |
| 类型选择 | 自制 Conditional |
std::conditional |
| 剥指针 | 自制 RemovePointer |
std::remove_pointer |
| 条件启用函数 | 自制 EnableIf(SFINAE) |
std::enable_if |
| 编译期断言 | 自制 StaticAssert |
static_assert(C++11) |
| 类型列表 | 自制 Typelist | 元组/std::variant 等 |
| 循环展开 | 递归模板成员函数 | 编译器自动优化 / constexpr |
最终一句话:C++98 模板元编程 = 用「特化做分支、递归做循环、typedef/枚举做变量」,在编译期处理类型和常量。理解了这三个武器和它们的组合方式,你就看懂了 C++ 模板最深邃的部分。

浙公网安备 33010602011771号