第六章:constexpr与编译期计算
constexpr 是 C++11 引入的关键字,到了 C++14/17/20 一路进化。说白了,它让「编译期干活」这件事变得非常自然。我刚开始接触时觉得它就是个加强版 const,后来才发现——这玩意儿能做的事远比你想象的多。
6.1 constexpr 函数与变量
先看最简单的用法:
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int val = square(5); // 编译期就算好了
int runtime = square(rand()); // 运行时才算
}
注意看,同一个函数既能编译期用,也能运行时用。这就是 constexpr 的妙处。我在项目中遇到过不少新人以为 constexpr 函数「只能」在编译期用,其实不是——它只是「可以」在编译期用。
核心规则:
- constexpr 函数体在 C++14 后可以包含循环、分支、局部变量
- 参数是编译期常量时,结果在编译期计算
- 参数是运行时变量时,退化为普通函数调用
嗯,这里要注意:C++11 的 constexpr 函数体只能有一条 return 语句,非常蛋疼。C++14 放宽了这个限制,所以如果你还在用 C++11,建议升级。
6.2 编译期字符串处理
字符串处理也能在编译期做?当然可以。你想想看,很多字符串操作其实不依赖运行时状态。
constexpr size_t strlen_c(const char* s) {
size_t len = 0;
while (s[len] != '\0') ++len;
return len;
}
constexpr bool is_palindrome(const char* s, size_t n) {
for (size_t i = 0; i < n / 2; ++i) {
if (s[i] != s[n - 1 - i]) return false;
}
return true;
}
int main() {
constexpr auto len = strlen_c("hello"); // 5
constexpr auto pal = is_palindrome("abcba", 5); // true
}
我曾经在写一个嵌入式协议解析器时,把所有字符串比较都挪到了编译期。效果很明显——运行时零开销,而且 bug 在编译阶段就暴露了。
小技巧:编译期字符串处理最适合做哈希计算、长度校验、模式匹配。别用它做动态分配或 I/O 操作,那是不可能的。
6.3 constexpr 容器
C++20 之前,标准库容器不能在 constexpr 上下文中使用。但你可以自己写一个简单的编译期容器:
template<typename T, size_t N>
class ConstexprArray {
T data[N] = {};
public:
constexpr ConstexprArray() = default;
constexpr T& operator[](size_t i) { return data[i]; }
constexpr const T& operator[](size_t i) const { return data[i]; }
constexpr size_t size() const { return N; }
constexpr T* begin() { return data; }
constexpr const T* begin() const { return data; }
constexpr T* end() { return data + N; }
constexpr const T* end() const { return data + N; }
};
constexpr int sum_array() {
ConstexprArray<int, 5> arr;
arr[0] = 1; arr[1] = 2; arr[2] = 3;
arr[3] = 4; arr[4] = 5;
int sum = 0;
for (auto v : arr) sum += v;
return sum;
}
static_assert(sum_array() == 15);
C++20 开始,std::vector 和 std::string 的部分操作也支持 constexpr 了。我个人习惯还是用自定义的小容器,因为标准库的 constexpr 支持还不够稳定。
注意:constexpr 容器不能使用动态内存分配(除非在编译期分配并在编译期释放)。C++20 放宽了一些限制,但依然有坑。我建议你写 constexpr 容器时,优先用栈上数组。
6.4 if constexpr
if constexpr 是 C++17 引入的编译期分支。它和普通 if 最大的区别是:不满足条件的分支根本不会被实例化。
template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // 只有 T 是指针时才编译
} else {
return t; // 否则走这里
}
}
int main() {
int x = 42;
int* p = &x;
auto v1 = get_value(x); // 42
auto v2 = get_value(p); // 42
}
为什么会这样?因为 if constexpr 在编译期就决定了走哪条路。另一条路的代码根本不会出现在生成的二进制里。这在模板元编程中特别有用。
我曾经在写一个序列化库时,用 if constexpr 处理不同类型:
template<typename T>
void serialize(const T& value) {
if constexpr (std::is_arithmetic_v<T>) {
// 直接写二进制
write_raw(&value, sizeof(T));
} else if constexpr (std::is_same_v<T, std::string>) {
// 先写长度,再写内容
write_size(value.size());
write_raw(value.data(), value.size());
} else {
// 递归序列化每个成员
for_each_member(value, [](auto& member) {
serialize(member);
});
}
}
嗯,这里要注意:if constexpr 的条件必须是编译期常量表达式。你不能写 if constexpr (x > 0) 这种,除非 x 是 constexpr 变量。
if constexpr 的典型应用场景:
- 类型特征分发(替代 SFINAE)
- 编译期算法选择
- 模板代码的编译期剪枝
- 减少模板实例化数量
6.5 知识体系总览
下面这张图总结了本章的核心内容:
这张图把 constexpr 的四个核心方向串起来了。你从上往下看,左边是函数与变量,中间是字符串处理,右边是 if constexpr,底部是容器。每个方向都有它的适用场景。
我的建议:刚开始用 constexpr 时,先从简单的函数做起。等你熟悉了编译期计算的思维模式,再尝试字符串处理和自定义容器。if constexpr 可以早点用,它比 SFINAE 好理解太多了。
constexpr 的核心价值在于:把运行时的工作提前到编译期。这不仅提升了性能,更重要的是——编译期能帮你发现更多 bug。我这些年写 C++ 最大的体会就是:能编译期做的事,绝不拖到运行时。
公众号:蓝海资料掘金营,微信deep3321