在 C++20 之前,constexpr 是我们实现编译期计算的唯一工具。它既可以用来标记变量,也可以用来标记函数,从而告诉编译器尝试在编译时求值。随着 consteval 的出现,C++ 进一步细化了编译期与运行期的界限。本文将从两者的语义、使用场景、实现细节以及典型误区四个方面进行系统阐述,并给出实战代码,帮助读者快速掌握两者的区别与正确使用方法。
1. 基本语义
| 关键词 | 作用 | 关键点 |
|---|---|---|
constexpr |
说明对象/函数可在编译期求值,但不强制 | 若编译器无法在编译期完成,退回到运行时 |
consteval |
强制在编译期求值 | 若无法在编译期完成,编译错误 |
constexpr 让函数可以在两种环境下被调用:编译期(如模板参数)和运行期。consteval 则完全排除了运行时执行的可能,任何调用都必须在编译期完成。
2. 使用场景
| 场景 | 推荐关键词 |
|---|---|
| 需要在编译期做数学运算,且函数会在运行时被调用 | constexpr |
| 需要确保某个函数只能在编译期使用,防止误用 | consteval |
| 需要在运行时进行复杂运算,且编译期不适用 | 纯普通函数 |
案例 1:常量表达式求斐波那契
constexpr unsigned long long fib(unsigned n) {
return (n < 2) ? n : fib(n-1) + fib(n-2);
}
constexpr unsigned long long fib5 = fib(5); // 5
这里 fib 可以在编译期计算 fib5,但如果我们在运行时调用 fib(30),编译器会把它当作运行时函数处理。
案例 2:强制编译期求值的 consteval
consteval int square(int x) {
return x * x;
}
int main() {
constexpr int val = square(7); // OK,编译期
int a = square(10); // ❌ 编译错误,必须在编译期
}
若你想让编译器在任何地方使用 square 时都强制执行编译期求值,consteval 是最佳选择。
3. 实现细节与约束
-
递归深度
consteval函数在编译期递归调用时,递归深度仍受编译器限制(通常为 512 或更少)。若递归深度过大,编译器会报“递归深度超限”。 -
异常
consteval函数不能抛异常。所有throw语句都会导致编译错误。constexpr函数可以抛异常,但在编译期调用时也会报错。 -
副作用
两者都不允许修改全局状态。consteval在这一点上更严格,任何修改都被视为非法。 -
函数体大小
constexpr允许函数体较大,只要满足编译期可执行。consteval在实际实现中对函数体大小并无额外限制,但编译器实现可能会因为资源限制产生警告。 -
返回类型
两者都要求返回类型是可以在编译期构造的(如int、std::array、std::string_view等)。constexpr允许返回非字面类型,只要它在编译期构造即可。
4. 常见误区
| 误区 | 正确理解 |
|---|---|
constexpr 就是 consteval |
不是,constexpr 允许运行时调用,consteval 必须在编译期 |
consteval 函数可以有副作用 |
错误,副作用导致编译错误 |
consteval 适合所有编译期计算 |
仅当你需要强制编译期时才用,过度使用会导致编译期计算复杂度高 |
constexpr 的变量一定是编译期求值 |
不是,如果初始化表达式在编译期不可行,则会退回到运行时 |
5. 综合实例:编译期数组生成
#include <array>
template<std::size_t N>
consteval std::array<int, N> make_array() {
std::array<int, N> arr{};
for (std::size_t i = 0; i < N; ++i) {
arr[i] = static_cast <int>(i * i);
}
return arr;
}
int main() {
constexpr auto arr = make_array <10>(); // 完全在编译期生成
static_assert(arr[3] == 9, "元素错误");
}
这里 make_array 必须在编译期返回完整数组,若在编译期无法完成(如 N 非常大),编译器会报错。
6. 结语
constexpr 与 consteval 分别提供了编译期与强制编译期两种工具。正确理解它们的语义差别、适用场景和限制,可以帮助我们编写更安全、更高效的 C++ 代码。尤其是在性能敏感或可配置性极高的系统中,合理利用这两者能显著提升运行时性能并减少错误。希望本文能成为你在 C++20 及以后版本中掌握常量表达式的实战指南。