本章目标:constexpr 深度使用、type_traits 实战、 if constexpr 实战、变参模板实战、SFINAE 概念、 编译期容器/计算。学完你就能"用编译器干活的程序员"。
普通编程:程序运行期计算。 元编程:编译器编译期计算——结果直接"焊死"进程序。
constexpr int fac(int n) { // 编译期可算阶乘
int r = 1;
for (int i = 2; i <= n; ++i) r *= i;
return r;
}
static_assert(fac(5) == 120); // 编译期断言:错了编译不过
好处:
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); // 编译期算好
#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>>; // 不能就存指针
#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 的三大用途:
if constexpr (std::is_pointer_v<T>) { use(*v); }
// 折叠表达式(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>);
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) {}
C++20 之后 constexpr 支持标准容器(部分):
constexpr std::vector<int> build_sieve() { ... }
// 前提:整个表达式在编译期求值
C++26 提供 std::type_list 和 pack_indexing,进一步简化:
// (你的 clang 21 可能已支持部分,此处只作概念)
需求: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 —— 程序启动前就算完,直接焊进二进制。
虚函数、std::cin、运行时库调用
练习题(配套测试:测试_第21章_元编程.cpp)