在 C++ 23 中,consteval 与 constinit 两个新关键字的引入,进一步强化了编译期计算与常量初始化的语义。它们虽然名字相似,但用途截然不同,掌握何时使用 consteval 能帮助我们写出更安全、更高效的代码。
1. consteval:强制编译期求值
consteval 用来修饰一个函数或构造函数,表示 必须 在编译期调用。若尝试在运行时调用,编译器将报错。它的主要作用是让编译器在编译阶段完成所有计算,避免运行时的成本,同时还能保证调用方的参数满足编译期约束。
consteval int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1);
}
使用示例:
constexpr int value = factorial(5); // OK,编译期计算
int arr[ factorial(3) ]; // OK,编译期数组大小
auto x = factorial(10); // OK,编译期返回值
auto y = []{ return factorial(3); }(); // OK,编译期调用 lambda
错误用法(编译错误):
int n = 5;
int a = factorial(n); // ❌ 需要在运行时计算,违反 consteval
2. constinit:保证全局/静态常量初始化
与 consteval 不同,constinit 用于变量声明,强制保证该变量在编译阶段完成初始化,并且初始化后 不能 发生更改。它解决了 constexpr 对全局对象的限制(constexpr 需要在声明时初始化),让我们可以写出更灵活的全局常量。
struct Config {
int value;
};
constinit Config cfg{42}; // 编译期初始化,且不可修改
若尝试修改:
cfg.value = 10; // ❌ 编译错误,constinit 只允许在初始化时赋值
3. 何时使用 consteval?
| 场景 | 适用关键字 |
|---|---|
| 需要在编译期完成所有计算,且函数不需要在运行时调用 | consteval |
| 想要在编译期做输入参数检查(例如,索引合法性) | consteval |
| 需要在编译期生成复杂的数据结构或配置 | consteval |
| 只需要保证全局或静态对象的初始化时机 | constinit |
示例:编译期生成哈希表
#include <array>
#include <cstddef>
#include <iostream>
consteval std::size_t simple_hash(const char* str) {
std::size_t h = 0;
while (*str) h = h * 31 + static_cast<std::size_t>(*str++);
return h;
}
consteval std::array<std::pair<const char*, int>, 3> init_table() {
return { { {"apple", 1},
{"banana", 2},
{"cherry", 3} } };
}
int main() {
constexpr auto table = init_table(); // 编译期生成
constexpr std::size_t h = simple_hash("banana");
for (auto&& [k, v] : table) {
if (simple_hash(k) == h) {
std::cout << k << " -> " << v << '\n';
}
}
}
在上述代码中,init_table 通过 consteval 在编译期创建了一个数组;simple_hash 也是 consteval,保证了所有哈希值在编译时就已知,从而极大地提高了运行时性能。
4. 小结
consteval:强制编译期求值,适合需要在编译期完成所有计算的函数。constinit:保证全局/静态对象在编译期初始化,避免运行时初始化。
正确使用这两个关键字,能够让我们的 C++ 代码在安全性与性能上得到显著提升。