编译期常量与常量表达式:constexpr 关键字、constexpr 函数、constexpr 与模板的结合
好,我们开始聊编译期常量这个话题。说实话,我刚接触 C++ 那会儿,对 const 和 constexpr 的区别也是一头雾水。那时候写模板,经常遇到「这个表达式不是常量表达式」的编译错误,气得想砸键盘。后来慢慢摸清了门道,才发现这玩意儿其实没那么玄乎。
constexpr 关键字:编译期的「通行证」
constexpr 是 C++11 引入的关键字。它的作用很简单:告诉编译器,这个值可以在编译期算出来。你想想看,如果编译器能在编译阶段就把结果算好,运行时就不用再算了,性能自然就上去了。
我个人习惯把 constexpr 理解成「编译期的承诺」。你承诺这个值是编译期可知的,编译器就会尽力去验证。如果验证通过,它就会在编译期把结果算好;如果验证失败,就会报错。
核心区别:
- const:运行时不可修改,但值不一定在编译期可知
- constexpr:编译期可知,且不可修改
// 这个没问题,编译期可知
constexpr int MAX_SIZE = 1024;
// 这个也没问题,编译期可知
int x = 42;
constexpr int SQUARE = x * x; // 错误!x 不是 constexpr
// 正确做法
constexpr int y = 42;
constexpr int SQUARE_OK = y * y; // 正确
嗯,这里要注意:constexpr 变量必须用常量表达式初始化。你不能拿一个运行时才能确定的变量去初始化 constexpr。
constexpr 函数:编译期的「计算器」
constexpr 函数就更有意思了。它允许你在编译期执行函数调用。我在项目中遇到过这样一个场景:需要根据配置参数计算缓冲区大小,如果能在编译期算好,就能避免动态内存分配。
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 编译期计算
constexpr int result = factorial(5); // 120
// 运行时也可以调用
int runtime_val = 5;
int runtime_result = factorial(runtime_val); // 也是 120
为什么会这样?因为 constexpr 函数既可以在编译期用,也可以在运行时用。关键在于参数:如果参数是常量表达式,编译器就会在编译期计算;如果参数是运行时变量,那就退化成普通函数调用。
我的经验:constexpr 函数的函数体必须足够简单。C++14 放宽了限制,允许在 constexpr 函数中使用局部变量、循环、分支等。但 C++11 的限制比较严格,只能有一条 return 语句。
// C++14 及以后,可以这样写
constexpr int factorial_cpp14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr 与模板的结合:编译期的「化学反应」
模板加上 constexpr,那才是真正的编译期计算利器。我曾经用这个组合实现过一个编译期字符串哈希,用来做 switch-case 的字符串匹配——说白了,就是把字符串在编译期转成整数,运行时直接比较整数,快得飞起。
// 编译期字符串哈希
template <size_t N>
constexpr size_t hash_string(const char (&str)[N]) {
size_t hash = 5381;
for (size_t i = 0; i < N - 1; ++i) {
hash = ((hash << 5) + hash) + str[i];
}
return hash;
}
// 使用示例
constexpr size_t hash_hello = hash_string("hello");
constexpr size_t hash_world = hash_string("world");
// 编译期 switch-case
void handle_command(const char* cmd) {
switch (hash_string(cmd)) {
case hash_string("start"):
// 处理 start
break;
case hash_string("stop"):
// 处理 stop
break;
}
}
你看,这里模板参数 N 是编译期推导的,hash_string 又是 constexpr 函数,两者结合就能在编译期算出哈希值。运行时只需要比较整数,效率极高。
避坑指南:我曾经在项目里用 constexpr 函数计算一个很大的数组,结果编译时间从 10 秒飙到了 3 分钟。原因是编译器在编译期递归展开,生成了大量模板实例。所以,constexpr 虽好,但别滥用。如果计算量太大,还是放到运行时吧。
编译期常量的实际应用
说了这么多理论,咱们看看实际项目中怎么用。我整理了几个常见场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 数组大小 | 编译期确定数组长度 | constexpr int N = 100; int arr[N]; |
| 模板参数 | 作为非类型模板参数 | template<int N> struct Foo {}; |
| 编译期计算 | 提前算好复杂表达式 | constexpr double PI = 3.14159; |
| 类型特征 | 配合 type_traits 使用 | constexpr bool is_int = std::is_integral_v<int>; |
我个人最常用的场景是模板参数。比如写一个编译期矩阵类,维度用 constexpr 表示,这样所有运算都能在编译期做类型检查,运行时零开销。
template <size_t Rows, size_t Cols>
class Matrix {
static_assert(Rows > 0 && Cols > 0, "Matrix dimensions must be positive");
constexpr size_t rows() const { return Rows; }
constexpr size_t cols() const { return Cols; }
// ...
};
// 使用
constexpr size_t N = 4;
Matrix<N, N> identity; // 4x4 矩阵
你看,constexpr 在这里既保证了编译期可知,又提供了类型安全。如果有人在运行时试图改变矩阵维度,编译器直接报错——这就是编译期检查的魅力。
总结一下
constexpr 是 C++ 模板元编程的基石之一。它让编译期计算变得简单、直观。记住三点:
- constexpr 变量必须用常量表达式初始化
- constexpr 函数可以编译期或运行时调用
- constexpr 与模板结合,能实现强大的编译期计算
嗯,这一章就到这里。下一章我们会聊更高级的编译期计算技巧,到时候你会看到 constexpr 和模板能碰撞出更多火花。
我的建议:刚开始用 constexpr 时,可以先从简单的常量开始。比如把魔法数字替换成 constexpr 变量。等熟悉了,再尝试 constexpr 函数。最后再挑战 constexpr + 模板的组合。一步一步来,别着急。
公众号:蓝海资料掘金营,微信deep3321