在 C++20 之前,constexpr 关键字已经让编译器在编译期间执行函数或生成常量值。然而,随着 constexpr 能够包含更多控制流和异常处理,编译器在运行时与编译时执行之间的界限逐渐模糊。C++20 引入了 consteval,为开发者提供了一个更严格的编译期执行声明。
1. constexpr 的现状
- 可在编译时或运行时执行:如果编译器无法在编译期间求值,
constexpr函数会在运行时调用。 - 语义宽松:即使函数内部有非 constexpr 表达式,编译器也可能在运行时调用。
- 兼容性好:大多数现有代码库只需将函数标记为
constexpr,不需要改动调用点。
2. consteval 的出现
- 强制编译期执行:任何使用
consteval的函数都必须在编译期间求值;否则编译器报错。 - 提高安全性:避免了意外的运行时调用,特别是在模板元编程或 constexpr 计算中。
- 性能保证:编译期求值可以消除运行时开销,尤其适用于需要在
static_assert或模板参数中的计算。
3. 什么时候使用 consteval?
| 场景 | 推荐关键字 | 理由 |
|---|---|---|
需要在模板参数、static_assert 等处保证求值 |
consteval |
防止错误的运行时调用 |
| 需要兼容运行时逻辑,且可以在编译时求值 | constexpr |
允许在无法求值时回退到运行时 |
| 需要确保所有使用点都在编译期间求值 | consteval |
例如在 constexpr 容器、数组大小等 |
4. consteval 与 constinit
constinit用于保证全局或静态变量在程序启动前已初始化为常量,但不强制在编译期间求值。它适用于需要在编译期间初始化但不一定是纯函数的场景。consteval则更像是constexpr的“编译期专用版”,其返回值不可能在运行时出现。
5. 示例代码
// 必须在编译期间求值,否则编译错误
consteval int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
// 允许运行时求值
constexpr int fib(int n) {
return n <= 1 ? n : fib(n-1) + fib(n-2);
}
int main() {
constexpr int f5 = factorial(5); // OK,编译期求值
static_assert(f5 == 120, "factorial wrong");
int f10 = factorial(10); // 编译错误:consteval 必须在编译期求值
// int f10 = fib(10); // OK,运行时求值
}
6. 小结
constexpr适合大多数需要编译期求值的情况,同时保持运行时回退的灵活性。consteval则是当你需要完全保证编译期求值,防止潜在运行时错误时的理想选择。- 在使用
consteval时,需要注意调用点是否也满足编译期求值的条件,否则编译器会报错。
合理选择 constexpr 与 consteval,可以让 C++20 的编译期计算既安全又高效,避免在模板元编程中潜藏的运行时成本。