跳到主要内容

编译期计算:constexpr 与模板元编程实用化

编译期计算的价值不在炫技,而在两件事:把错误提前到编译期、把开销从运行时挪走。

一、static_assert:编译期断言​

static_assert(sizeof(void*) == 8, "需要 64 位平台");
static_assert(std::is_trivially_copyable<T>::value, "T 必须可平凡拷贝");

C++11 的 static_assert 必须带消息;C++17 起消息可省略。用在:

  • 结构体布局锁定(见内存对齐篇);
  • 模板实参约束(C++20 用 requires,之前用 static_assert + 类型萃取);
  • 平台假设(字节序、指针宽度、long 宽度)。

二、constexpr 函数​

// C++11:单 return 语句
constexpr int square(int x) { return x * x; }

// C++14:任意语句
constexpr std::size_t strLen(const char* s) {
std::size_t n = 0;
while (s[n]) ++n;
return n;
}

constexpr std::size_t k = strLen("hello"); // 编译期 5

注意:constexpr 函数在参数不是常量表达式时退化为运行时执行,不会报错。要用 static_assert 强制编译期求值:

static_assert(square(4) == 16, "编译期校验");

三、constexpr 的实用场景​

场景 1:表驱动替代运行时计算​

constexpr std::array<int, 256> makeCrcTable() {
std::array<int, 256> t{};
for (int i = 0; i < 256; ++i) {
int c = i;
for (int j = 0; j < 8; ++j) c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1);
t[i] = c;
}
return t;
}
constexpr auto kCrcTable = makeCrcTable(); // 编译期生成,运行时零成本

这就是"编译期建表"的现代写法,替代了以前用脚本生成头文件的做法。

场景 2:编译期字符串哈希​

constexpr std::uint32_t fnv1a(const char* s, std::size_t n) {
std::uint32_t h = 2166136261u;
for (std::size_t i = 0; i < n; ++i) {
h ^= static_cast<unsigned char>(s[i]);
h *= 16777619u;
}
return h;
}
constexpr std::uint32_t operator""_hash(const char* s, std::size_t n) {
return fnv1a(s, n);
}

switch (key) {
case "alpha"_hash: /* ... */ break; // 编译期算好,无运行时开销
case "beta"_hash: /* ... */ break;
}

四、模板特化与 if constexpr(C++17)​

C++11 时代的模板分支靠特化与重载,写起来啰嗦:

// C++11:靠重载做类型分支
template <class T> struct Serializer { static void write(T v) { rawWrite(v); } };
template <> struct Serializer<std::string> { static void write(const std::string& s) { rawWrite(s.data(), s.size()); } };

C++17 起用 if constexpr:

template <class T>
void write(const T& v) {
if constexpr (std::is_same_v<T, std::string>) {
rawWrite(v.data(), v.size());
} else if constexpr (std::is_arithmetic_v<T>) {
rawWrite(&v, sizeof(v));
} else {
static_assert(sizeof(T) == 0, "unsupported type");
}
}

if constexpr 丢弃的分支不会被实例化,因此不会触发无意义的编译错误。

五、类型萃取(type traits)常用清单​

萃取用途
std::is_integral / is_floating_point数值类型分支
std::is_same / is_convertible类型匹配
std::is_trivially_copyable能否 memcpy
std::is_nothrow_move_constructible容器扩容策略
std::enable_ifSFINAE 重载约束(C++11)
std::decay_t退化推导结果(C++14)
std::remove_reference_t去掉引用(C++14)

_t 与 _v 后缀是 C++14/17 的简写形式,老代码里的 typename std::is_same<A,B>::type 可逐步替换。

六、编译期递归与循环​

C++11 没有 constexpr for,模板元编程靠递归:

template <int N> struct Fact {
static constexpr long long value = N * Fact<N - 1>::value;
};
template <> struct Fact<0> {
static constexpr long long value = 1;
};

C++14 起直接写循环即可,上面的代码可以简化为:

constexpr long long fact(int n) {
long long r = 1;
for (int i = 2; i <= n; ++i) r *= i;
return r;
}

结论:新代码尽量用 constexpr 函数,模板元编程只在需要"类型级计算"时才用。

七、编译期与运行时开销对照​

手段编译期运行时适用
宏无检查展开后同函数仅条件编译,别用来算数
constexpr 函数求值零(常量表达式处)计算常量、建表
模板特化实例化零(直接内联)类型相关逻辑
虚函数无一次间接跳转运行时多态
std::function无类型擦除 + 可能堆分配存储回调

八、代价与边界​

  • 编译时间上升:大量模板实例化与 constexpr 求值会拖慢编译,注意用前置声明与显式实例化隔离;
  • 报错可读性下降:模板错误信息冗长,用 static_assert 提供人类可读的诊断;
  • 调试困难:constexpr 中间值在调试器中可能不可见;
  • 过度设计:不是所有常量都值得 constexpr,const int 足够清楚时就用它。

九、实战建议​

  1. 先用 constexpr 函数替代"计算型宏",零成本改造;
  2. 给关键结构加 static_assert 锁布局与大小;
  3. 表驱动逻辑(CRC、查表、映射)改编译期生成;
  4. 类型分支优先 if constexpr(C++17)而非特化;
  5. static_assert(sizeof(T) == 0, ...) 作为"未支持类型"的兜底诊断。

编译期做的事越多,运行时能出的错就越少。