在 C++20 之前,constexpr 已经成为了编译期常量的标识符,允许在编译时求值的函数与变量。但随着 C++20 的发布,新增了 consteval 关键字,它在功能上与 constexpr 有细微而重要的区别。下面从概念、语义、适用场景以及典型使用案例四个方面详细剖析这两者的差异。
1. 概念与语义
| 关键字 | 作用 | 评估时机 | 允许的操作 | 错误行为 |
|---|---|---|---|---|
constexpr |
声明编译期可求值的实体 | 编译期 可选,若无法在编译期求值,则退回运行时 | 仅允许编译期可求值的表达式;若失败,只影响编译期求值 | 无编译错误,编译器可在运行时生成代码 |
consteval |
声明 必须 在编译期求值的实体 | 必须在编译期 | 与 constexpr 相同,但若不满足编译期求值条件则直接报错 |
编译错误,阻止生成运行时代码 |
简而言之,constexpr 只保证 在编译期能够求值,但不强制;而 consteval 强制 一定在编译期求值。如果一个 consteval 函数在调用时传入的实参无法在编译期求值,编译器会立即报错,而 constexpr 则会退回到运行时执行。
2. 适用场景
| 场景 | 适合使用 constexpr |
适合使用 consteval |
|---|---|---|
| 函数可以在编译期或运行时执行 | 如常用的 std::max、std::min、数学运算等 |
不适用 |
| 需要保证函数在编译期必定求值 | 如宏式编译器选项、编译期数组大小计算 | 如类型大小计算、编译期字符串拼接等 |
| 编译期错误提示 | 编译期错误仅作为警告 | 编译期错误会导致编译失败,提示开发者使用不当 |
| 性能优化 | 允许在编译期求值但也可在运行时执行,避免不必要的编译期开销 | 需要确保所有调用都在编译期完成,避免运行时开销 |
3. 典型使用案例
3.1 用 constexpr 实现通用 gcd(最大公约数)
constexpr int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
此函数既可以在编译期计算 gcd(12, 18),也可以在运行时使用 gcd(x, y)。
3.2 用 consteval 实现编译期字符串拼接
consteval const char* concat(const char* a, const char* b) {
static char buffer[256];
// 简化实现,真实使用中应考虑长度检查
std::size_t i = 0;
while (a[i] != '\0') buffer[i] = a[i++];
std::size_t j = 0;
while (b[j] != '\0') buffer[i++] = b[j++];
buffer[i] = '\0';
return buffer;
}
constexpr const char* full = concat("Hello, ", "world!"); // 编译期求值
如果我们尝试 consteval concat(s1, s2) 但 s1、s2 在运行时才确定,编译器将报错。
3.3 用 consteval 检查数组大小是否合法
consteval void check_size(std::size_t size) {
if (size == 0) throw "Array size must be > 0";
}
consteval void use_array(std::size_t n) {
check_size(n);
int arr[n];
}
在调用 use_array(0) 时编译会立即报错,防止产生无效数组。
4. 编译器差异与兼容性
- 大多数现代编译器(GCC 10+、Clang 10+、MSVC 16+)已完全支持
consteval。 consteval函数若出现运行时错误(如异常抛出),编译器会在编译期生成相应错误。- 对于旧编译器,需要使用宏或条件编译来降级为
constexpr或传统实现。
5. 小结
constexpr:可选编译期求值,若不满足则退回运行时。consteval:强制编译期求值,任何不满足条件的调用都会导致编译错误。- 在需要强制保证编译期行为、提升错误可见性时使用
consteval;在既想获得编译期优化又允许运行时执行时使用constexpr。
掌握这两者的区别与使用时机,可以让 C++ 程序员在编写高性能、类型安全的代码时更加得心应手。