在 C++20 之前,constexpr 关键字已经为我们提供了在编译期求值的能力,但它并不强制在编译期求值,更多的是一种“建议”。从 C++20 开始引入的 consteval 则彻底把函数的求值时机锁定在编译期,任何运行时调用都会导致编译错误。下面我们分别梳理两者的语义差异、适用场景,并给出实际代码示例,帮助开发者在实践中更好地选择使用。
1. 语义对比
| 关键字 | 编译期求值强制性 | 运行时是否允许 | 适用对象 |
|---|---|---|---|
constexpr |
允许,但不强制 | 允许 | 函数、变量、构造函数等 |
consteval |
强制 | 不允许 | 函数(不能声明为 consteval 的变量) |
constexpr:如果函数或变量在使用时能在编译期求值,编译器会尝试这么做;如果不行,则退回到运行时。此时编译器并不会报错,但会在使用处出现运行时计算。consteval:编译器必须在编译期求值,若不能,则编译失败。编译器在遇到consteval函数调用时,任何使用都被强制要求在编译期完成。
2. 实际使用场景
| 场景 | 推荐关键字 | 说明 |
|---|---|---|
| 需要在编译期计算常量,并希望编译器在必要时退回到运行时 | constexpr |
例如模板元编程中的阶乘,某些值不一定能在编译期确定。 |
必须保证函数在编译期求值,以便其结果用于 constexpr 变量或 static_assert |
consteval |
用来构造真正的编译期常量,防止误用。 |
| 想在编译期检测某些属性但不需要返回值 | constexpr + static_assert |
通过 constexpr 函数返回 true/false,再在 static_assert 中使用。 |
| 编译期数组大小 | constexpr |
传统用法,返回数组大小。 |
3. 代码示例
3.1 constexpr 计算阶乘
constexpr unsigned long long factorial(unsigned int n) {
return n <= 1 ? 1ULL : n * factorial(n - 1);
}
constexpr unsigned long long fact5 = factorial(5); // 120 在编译期求值
static_assert(fact5 == 120, "阶乘错误");
3.2 consteval 强制编译期
consteval int compute(int x) {
return x * 2; // 必须在编译期求值
}
constexpr int y = compute(10); // 正常
int z = compute(10); // 编译错误:consteval 函数不能在运行时调用
3.3 consteval 与 constexpr 混合使用
constexpr int square(int x) { return x * x; } // 允许编译期或运行时
consteval int cube(int x) { return x * x * x; } // 必须编译期
constexpr int s = square(5); // 25 在编译期求值
static_assert(cube(3) == 27, "立方错误"); // 编译期检查
4. 常见陷阱
-
constexpr变量不一定是常量表达式
由于constexpr变量本身是常量,但其初始化表达式若不是常量表达式,编译器会退回到运行时。例如:constexpr int a = std::rand(); // 编译器报错:rand 不是常量表达式 -
consteval不能作为类成员
consteval只能修饰自由函数,不能修饰成员函数或构造函数。 -
编译器实现差异
对于constexpr,不同编译器在推导时机上的实现可能略有差异,尤其是 C++17 之前的标准允许更宽松的求值策略。
5. 结语
- 若你只需要 可能 在编译期求值,或者想让编译器在不支持时退回到运行时,使用
constexpr。 - 若你想强制编译期求值,避免任何运行时调用,或者在
static_assert等上下文中使用,选择consteval。
掌握两者的区别与适用场景,可以让你在写高效、可靠的 C++ 代码时更加游刃有余。