在 C++20 之前,constexpr 用来指示一个表达式可以在编译期求值,编译器会在需要时尝试对其进行编译期求值;而在运行时如果编译器无法确定其值,仍然会以运行时方式执行。C++20 引入了 consteval,它完全要求在编译期求值,任何非编译期调用都会导致编译错误。下面从语义、使用场景以及两者的交互来深入探讨这两种限定符。
1. 基本语法与语义
constexpr int add(int a, int b) {
return a + b; // 该函数可以在编译期或运行期调用
}
consteval int mul(int a, int b) {
return a * b; // 必须在编译期调用
}
- constexpr:可以在编译期求值,也可以在运行时求值。它的返回值类型必须是字面量类型或引用。
- consteval:只能在编译期调用。若尝试在运行时调用,则编译器报错。返回值必须是字面量类型。
2. 何时使用 constexpr?
- 需要编译期求值,但也希望在运行时可用:例如一个通用的
max函数,既能在模板元编程中计算,也能在普通运行时调用。 - 与标准库中的
constexpr函数兼容:如std::sqrt、std::abs等已被声明为constexpr的函数。
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n-1));
}
int main() {
constexpr int fact5 = factorial(5); // 编译期求值
int arr[fact5]; // 可用作数组大小
std::cout << factorial(10) << '\n'; // 运行时求值
}
3. 何时使用 consteval?
- 强制编译期求值:当你想确保某个函数只能在编译期使用,以避免不必要的运行时开销或错误调用。
- 实现编译期常量:例如在生成编译期常量表、验证参数等场景。
- 配合
if constexpr:consteval函数可用于决定模板分支的编译期路径。
consteval int computeTableSize(int entries) {
if (entries <= 0) {
throw "entries must be positive";
}
return entries * 2; // 仅在编译期可见
}
int main() {
constexpr int tableSize = computeTableSize(256); // 编译期
int table[tableSize]; // 合法
// int bad = computeTableSize(-5); // 编译错误
}
4. 两者的交互与注意事项
- consteval 函数不能返回非字面量类型。例如不能返回
std::string,因为它不是字面量类型。 - constexpr 函数可以返回非字面量类型,只要满足字面量构造。例如返回
std::array<int, N>。 - consteval 函数可以调用 constexpr 函数,但反之不成立。
- 在模板元编程中,
consteval可以配合requires子句,确保模板参数满足编译期约束。
template<int N>
requires consteval (N > 0)
struct ArrayHolder {
int arr[N];
};
5. 性能与编译器支持
- 大多数主流编译器(GCC 11+, Clang 13+, MSVC 19.32+)已完整实现 C++20 的
consteval。 consteval的强制编译期求值往往会让编译时间略有增长,但可显著降低运行时开销,尤其在需要大规模常量计算时更为明显。
6. 小结
| 特性 | 是否可运行时求值 | 是否必须编译期 | 适用场景 |
|---|---|---|---|
| constexpr | ✅ | ❌ | 需要兼容运行时调用,或在标准库中常见 |
| consteval | ❌ | ✅ | 强制编译期执行,确保无运行时开销,适合参数验证、编译期表计算等 |
实用建议:在实现库接口时,优先使用
constexpr,保持兼容性;当你需要完全的编译期保证,或想让错误更早被捕获时,再考虑使用consteval。记住,consteval是“如果你不想让它在运行时存在,那就用它”。