第二十一章 元编程:让编译器帮你写代码

← 返回目录

本章目标:constexpr 深度使用、type_traits 实战、 if constexpr 实战、变参模板实战、SFINAE 概念、 编译期容器/计算。学完你就能"用编译器干活的程序员"。

21.1 元编程是什么(人话)

普通编程:程序运行期计算。 元编程:编译器编译期计算——结果直接"焊死"进程序。

constexpr int fac(int n) {        // 编译期可算阶乘
    int r = 1;
    for (int i = 2; i <= n; ++i) r *= i;
    return r;
}
static_assert(fac(5) == 120);     // 编译期断言:错了编译不过

好处:

  1. 零运行期开销(表、常量、分派全在编译期完成)
  2. 类型级安全检查(static_assert 挡住错误用法)
  3. 减少重复代码(一套模板,自动生成 N 份)

21.2 constexpr 的进化(从 C++11 到 C++14/17/20)

constexpr 支持容器(vector 等可在编译期用)、 constexpr new(有限制)

现代用法(C++17+):

constexpr auto fib_seq(int n) {
    std::array<int, 10> arr{};
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) { arr[i] = a; int t = a + b; a = b; b = t; }
    return arr;
}
constexpr auto fib = fib_seq(10);   // 编译期算好

21.3 type_traits 实战:编译期"类型问答"

#include <type_traits>
using P = std::remove_cvref_t<const int&>;   // C++20:去掉 const/volatile/引用
static_assert(std::is_same_v<P, int>);

// 常用 traits 表(全部 _v 后缀为 C++17 值版本)
std::is_integral_v<T>        // 整数?
std::is_floating_point_v<T>  // 浮点?
std::is_pointer_v<T>         // 指针?
std::is_reference_v<T>       // 引用?
std::is_const_v<T>           // const?
std::is_same_v<A, B>         // 同类型?
std::is_base_of_v<B, D>      // D 继承自 B?
std::is_nothrow_move_constructible_v<T>  // 移动不抛?
std::rank_v<T>               // 几维数组
std::decay_t<T>              // 去引用/const → 值类型
std::conditional_t<B, T, F>  // 编译期 if-else(类型)

conditional 的例子:

template <typename T>
using storage = std::conditional_t<
    std::is_trivially_copyable_v<T>,   // 能 memcpy 吗?
    T,                                 // 能就存值
    std::unique_ptr<T>>;               // 不能就存指针

21.4 if constexpr 实战:分派与代码消灭

#include <concepts>
template <typename T>
auto describe(const T& v) {
    if constexpr (std::integral<T>) {
        return "整数";
    } else if constexpr (std::same_as<T, double>) {
        return "double";
    } else {
        return "其他";
    }
}
// 每个分支的 return 类型可以不同!
// 返回值类型由"实际生效的分支"决定(C++17 要求所有分支
// 类型一致或使用 auto;不同分支类型不同时,
// C++17 不行,要 C++20 以后某些编译器放宽?——实际上
// 这是错误示范,不同 return 类型在 if constexpr 里也不允许,
// 需要把返回写成 const char*)

修正版本(各分支同类型):

template <typename T>
const char* describe(const T& v) {
    if constexpr (std::integral<T>) return "整数";
    else if constexpr (std::same_as<T, double>) return "double";
    else return "其他";
}

if constexpr 的三大用途:

  1. 类型分派(上面)
  2. 防止非法表达式被编译:
   if constexpr (std::is_pointer_v<T>) { use(*v); }
  1. 生成/删除代码:false 分支的代码不存在

21.5 变参模板实战:编译期递归与折叠

// 折叠表达式(C++17,最简洁)
template <typename... Ts>
auto sum(Ts... args) { return (args + ...); }

// 编译期长度
static_assert(sizeof...(int, double, char) == 3);

// 一个参数一个参数地处理(index 打印)
template <typename... Ts>
void print_all(Ts... args) {
    (std::println("{}", args), ...);      // 逗号折叠
}

// 参数包的类型列表(C++26 可以用 std::type_list 简化,
// 这里用传统方式):
template <typename... Ts>
struct TypeList {
    static constexpr std::size_t size = sizeof...(Ts);
    template <std::size_t I> using at = std::tuple_element_t<I, std::tuple<Ts...>>;
};
using L = TypeList<int, double, char>;
static_assert(L::size == 3);
static_assert(std::is_same_v<L::at<1>, double>);

21.6 SFINAE:替换失败不是错误(理解概念)

SFINAE = Substitution Failure Is Not An Error (替换失败不是错误)。

当模板实例化时,某个重载/特化"替换后无效", 编译器不报错,而是把该候选"划掉",继续找别的。

// 例:只为整数类型提供这个函数
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void only_int(T v) { /* 整数专用 */ }

// only_int(3.5) → 替换失败 → 该候选被划掉
// → 没有匹配 → 编译错误(没有 other candidate)

C++20 之后:concepts + requires 取代了 90% 的 SFINAE 写法, 报错信息还更好。SFINAE 只需要"认识",不推荐新写。

// 现代写法(concepts)
template <std::integral T>
void only_int(T v) {}

21.7 编译期数据结构:元编程的高级形态

C++20 之后 constexpr 支持标准容器(部分):

constexpr std::vector<int> build_sieve() { ... }
// 前提:整个表达式在编译期求值

C++26 提供 std::type_list 和 pack_indexing,进一步简化:

// (你的 clang 21 可能已支持部分,此处只作概念)

21.8 实战:编译期查找表(性能武器)

需求:sin 查表(避免运行时计算,极快)

constexpr double pi = 3.14159265358979;

constexpr auto make_sin_table(int n) {
    std::array<double, 1024> table{};
    for (int i = 0; i < 1024; ++i)
        table[i] = std::sin(pi * i / 1024.0);
    return table;
}
constexpr auto SIN_TABLE = make_sin_table(1024);
// 查表:SIN_TABLE[i] 零运行期成本

编译期算 1024 个 sin —— 程序启动前就算完,直接焊进二进制。

21.9 陷阱清单

  1. constexpr 函数里不能用:动态分配(C++20 前)、

虚函数、std::cin、运行时库调用

  1. if constexpr 的 false 分支"不生成",但语法要合法
  2. 别把元编程写成一坨"无法阅读"的模板——先用概念/auto
  3. 模板报错先看第一条 error 的最后几行(真正的根因)
  4. 编译期计算有极限(编译时间/内存),超大表用运行时
  5. constexpr 深度嵌套会爆编译器栈:注意循环/递归深度

本章小结

练习题(配套测试:测试_第21章_元编程.cpp)


  1. 用 constexpr 实现编译期 pow(整数幂)。
  2. 编译期生成 1..100 的素数数组(static_assert 验证)。
  3. 用 if constexpr 写一个"打印任意值"的函数(处理指针/整数/字符串)。
  4. 写一个 TypeList 并实现 size/at/get。
  5. 编译期查表:费波那契前 30 项存数组。