跳转至

Template Metaprogramming

模板元编程(TMP,Template Metaprogramming)是 利用模板在编译期进行计算 的技术。它把编译器当成一个"解释器",让程序的一部分逻辑在编译阶段就执行完毕,而不是等到运行期

  • 普通程序:代码 → 编译 → 运行时 计算
  • 元编程:代码 → 编译时 计算(模板展开)→ 运行时代码里只留下结果

一个最经典的例子——编译期计算阶乘

// 递归模板:N! = N * (N-1)!
template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

// 特化:递归终止条件(0! = 1)
template <>
struct Factorial<0> {
    static const int value = 1;
};

int main() {
    int x = Factorial<5>::value;   // 120
    // 编译器在编译期就展开为字面量 120,运行时零开销
}

这段代码揭示了 TMP 的 三大支柱

机制 作用 类比
递归模板 循环/递归 函数的递归调用
模板特化 终止条件/分支 if 语句
模板参数 输入 函数参数

有趣的历史:模板的图灵完备性是 1994 年 Erwin Unruh 意外发现的——他写了一个在 编译错误信息里打印素数 的程序,证明模板能在编译期完成任意计算

1 值计算:整型与枚举

模板元编程主要处理两类"值":整型常量类型

1.1 用 static constenum 承载结果

template <int N>
struct Fibonacci {
    // enum 是早期做法(不占内存,且旧编译器支持好)
    enum { value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value };
};

template <>
struct Fibonacci<0> { enum { value = 0 }; };

template <>
struct Fibonacci<1> { enum { value = 1 }; };

int x = Fibonacci<10>::value;   // 55

1.2 现代写法:继承 std::integral_constant

#include <type_traits>

template <int N>
struct Factorial : std::integral_constant<int, N * Factorial<N - 1>::value> {};

template <>
struct Factorial<0> : std::integral_constant<int, 1> {};

// std::integral_constant<int, V> 自带:
//   static constexpr int value = V;
//   operator int()  → 可隐式转换
//   operator()      → 可像函数一样调用

2 类型计算:Type Traits

元编程不只算数字,更常 操纵类型——在编译期"传入类型、产出类型":

// 移除 const:通用版本原样返回
template <typename T>
struct RemoveConst {
    using type = T;
};

// 特化:匹配 const T,去掉 const
template <typename T>
struct RemoveConst<const T> {
    using type = T;
};

RemoveConst<const int>::type  x;   // x 是 int
RemoveConst<double>::type     y;  // y 是 double

标准库 <type_traits> 提供了大量现成的类型运算:

Trait 作用
std::remove_const<T> / remove_reference<T> 移除 const / 引用
std::decay<T> 类型退化(数组→指针、函数→函数指针等)
std::conditional<B, T, F> 编译期 if:B 为真取 T,否则取 F
std::is_integral<T> / is_pointer<T> 类型判断,产出 true_type/false_type
std::enable_if<B, T> 条件启用重载(见下)
std::conditional<true, int, double>::type a;    // int
std::conditional<false, int, double>::type b;   // double

3 SFINAE:替换失败不是错误

SFINAE(Substitution Failure Is Not An Error)是模板元编程最重要的规则之一:

模板参数推导过程中,如果某个候选模板的"替换"失败了,编译器 不会报错,而是静默地把它从候选集里移除,继续尝试其他候选

这让我们能 在编译期根据类型特性选择不同的重载

// 仅当 T 是整型时,这个重载才参与匹配
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
add(T a, T b) { return a + b; }     // 整型:直接加

// 仅当 T 是浮点型时参与匹配
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
add(T a, T b) { return a + b + 0.5; }  // 浮点:额外逻辑

add(1, 2);     // 调用整型版本
add(1.0, 2.0); // 调用浮点版本

3.1 std::enable_if 的几种用法

// ① 用作返回类型
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
f(T t) { /* ... */ }

// ② 用作额外的模板参数(默认参数)
template <typename T,
          typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
void f(T t) { /* ... */ }

// ③ C++14 起可用 _t 后缀简化
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void> f(T t) { /* ... */ }

4 变参模板:处理任意多个参数

C++11 的变参模板让元编程可以 递归地拆解参数包

// 递归出口:没有参数
void printAll() {}

// 打印第一个参数,再递归处理剩余
template <typename T, typename... Args>
void printAll(T first, Args... rest) {
    std::cout << first << ' ';
    printAll(rest...);
}

printAll(1, 2.5, "hello", 'c');   // 1 2.5 hello c

编译期求和:

template <typename... Args>
struct Sum;

template <typename T, typename... Rest>
struct Sum<T, Rest...> {
    static const int value = T::value + Sum<Rest...>::value;
};

template <>
struct Sum<> {
    static const int value = 0;
};

5 现代 C++ 的简化:constexprif constexpr

模板元编程曾经晦涩难懂,现代 C++ 提供了更直白的工具

5.1 constexpr 函数

C++11/14/17 的 constexpr 函数允许用 普通函数语法 做编译期计算,不必再写递归模板:

1
2
3
4
5
6
7
8
9
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i)   // C++14 起允许循环
        result *= i;
    return result;
}

int arr[factorial(5)];   // 编译期算出 120,用作数组大小
static_assert(factorial(5) == 120);

5.2 if constexpr(C++17)

if constexpr 在编译期做分支,被舍弃的分支 不会被实例化,大大简化了 SFINAE:

template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;          // T 是指针时,只有这分支被编译
    } else {
        return t;
    }
}

int x = 5;
getValue(&x);   // 返回 5(解引用)
getValue(x);    // 返回 5(原样)

if constexpr vs 普通 if

普通 if 的两个分支 都会 被编译(只是运行期只执行一个);if constexpr 只会实例化**满足条件的那个分支**,因此可以在不同分支里写"对某些类型根本编译不过"的代码。

6 完整实例:编译期判断类型并分发

综合运用上述机制:

#include <type_traits>
#include <iostream>

// 通用版本
template <typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "整数:" << value << '\n';
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "浮点:" << value << '\n';
    } else if constexpr (std::is_pointer_v<T>) {
        std::cout << "指针指向:" << *value << '\n';
    } else {
        std::cout << "其他类型\n";
    }
}

int main() {
    process(42);        // 整数:42
    process(3.14);      // 浮点:3.14
    int x = 7;
    process(&x);        // 指针指向:7
}

评论区

欢迎在评论区指出文档错误,为文档提供宝贵意见,或写下你的疑问