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 年还要学它?

  1. 大量现存代码库(Boost、旧项目、游戏引擎)是 C++98 时代写的,你要能读懂;
  2. C++11 之后的 constexpr 能替代大部分"算数值"的元编程,但操作类型这件事至今只能靠模板——这是 TMP 至今不过时的核心价值;
  3. 理解了 TMP,你才能真正看懂 std::enable_ifstd::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 的模板元编程只有三件武器,全部工具都由它们组合而成:

  1. 模板特化/偏特化(做"编译期 if"和"分支")
  2. 模板递归实例化(做"编译期循环")
  3. 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。

两个必背要点

  1. 递归必须有特化版出口(否则编译器递归到死、报 recursive template instantiation exceeded maximum depth);
  2. 递归的每一层都是一个独立的类型,多一层就多一份编译负担——这也是为什么元编程会拖慢编译。

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 关键语法 typenameList::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++ 的特性:

  1. 语法丑陋typename::typeenum{value}>> 之间要加空格,可读性差;
  2. 没有 if constexpr:条件分支全靠特化/偏特化,"编译期短路"不存在,所有分支都实例化(慢);
  3. 算数值只能用整型:算 FactorialSum 勉强,算浮点/字符串/复杂表达式非常痛苦;
  4. 调试地狱:编译错误信息堆叠成百行模板递归,人很难定位。

出路: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++ 模板最深邃的部分。

posted @ 2026-09-04 16:52  IcarusLee  阅读(5)  评论(0)    收藏  举报