在 C++20 之前,编译期常量的定义方式主要是 constexpr,但它既可以用于运行时,也可以用于编译期,导致在某些场景下使用不够精准。C++20 新增了两个关键字——consteval 与 constinit,分别为 “必定在编译期求值” 与 “在编译期初始化但不强制求值” 提供了更细粒度的控制。下面我们从语义、使用场景、性能以及常见坑四个方面深入探讨这两个关键字。
1. 语义对比
| 关键字 |
必要条件 |
适用对象 |
运行时行为 |
编译期行为 |
constexpr |
需要可在编译期求值,若不满足则退化为运行时 |
函数、变量、类、成员函数 |
可以在运行时执行 |
若满足条件可在编译期求值 |
consteval |
强制 在编译期求值,否则编译错误 |
函数、变量、类、成员函数 |
永不在运行时 |
必须在编译期求值 |
constinit |
仅要求编译期初始化,不强制求值 |
变量(全局/静态) |
在运行时可读写 |
必须在编译期初始化,但可延迟求值 |
consteval 的本质是“编译期函数”。任何 consteval 函数的调用都必须在编译期完成,否则编译器会报错。
constinit 仅用于变量,保证在程序启动前完成初始化,防止因未初始化导致的未定义行为。它不保证初始化值是常量表达式,而是允许使用 constexpr 或普通表达式,只要能够在编译期完成即可。
2. 使用场景
2.1 consteval 适用场景
| 场景 |
示例 |
| 需要在编译期生成值,并防止错误使用 |
consteval int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } |
| 计算数组大小、字符串长度等不可变值 |
constexpr const char* hello = "Hello"; constexpr int len = consteval strlen(hello); |
| 设计编译期容器或映射 |
template<int N> struct static_vector { int data[N]; }; 使用 consteval 构造函数限制编译期传参 |
2.2 constinit 适用场景
| 场景 |
示例 |
| 全局或静态变量需要在程序启动前初始化 |
`constinit std::vector |
| globalVec = {1,2,3};` |
| 防止“静态初始化顺序”问题 |
struct A { static constinit int x = 42; }; |
| 配置常量但不想强制其为常量表达式 |
constinit std::string logLevel = readConfig("log_level"); // readConfig 在编译期读取配置文件 |
3. 性能与实现细节
-
consteval: 由于必须在编译期求值,编译器在生成代码时会直接把计算结果嵌入到目标代码中,类似 constexpr 的处理,但更严格。若函数体中出现无法在编译期求值的语句(如 throw、动态内存分配等),编译会失败。
-
constinit: 只保证初始化在编译期完成,编译器可以将初始化表达式转化为静态构造函数,或者直接在 BSS/RODATA 区段中放入常量值。对运行时没有额外开销,避免了全局变量在运行时初始化的“可见性”问题。
4. 常见坑 & 解决方案
| 坑 |
说明 |
解决方案 |
consteval 函数返回非 constexpr 类型 |
编译器报错:返回类型不是常量表达式 |
确认返回值类型满足 constexpr 要求,或改为 consteval auto |
constinit 与 constexpr 冲突 |
同一个变量既标记为 constinit 又用 constexpr 进行初始化 |
两者兼容,但 constinit 的 constexpr 只是保证可在编译期求值,最好保持一致 |
使用 consteval 计算文件大小 |
consteval std::size_t file_size(const char* path) 读取文件 |
读取文件在编译期不被允许,需要用 std::filesystem 结合 constexpr 在编译期读取,或者改为构建系统 |
全局 constinit 变量与线程安全 |
线程创建时访问未初始化的全局对象 |
constinit 确保初始化先于任何线程访问,但若在多线程构造函数中使用未锁定资源,需要自行同步 |
5. 代码示例
#include <iostream>
#include <string>
#include <vector>
#include <type_traits>
/* 1. consteval 示例:编译期阶乘 */
consteval int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
/* 2. constinit 示例:全局容器初始化 */
struct Config {
static constinit std::vector <int> values = {1, 2, 3, 4, 5};
};
int main() {
// 直接使用 consteval 结果
constexpr int fact5 = factorial(5);
std::cout << "5! = " << fact5 << '\n';
// 访问 constinit 初始化的全局变量
for (auto v : Config::values) {
std::cout << v << ' ';
}
std::cout << '\n';
}
运行结果:
5! = 120
1 2 3 4 5
6. 小结
consteval 让你在编译期强制求值,适合需要在编译时得到确定值的函数与变量。
constinit 只保证变量在程序启动前完成初始化,解决全局/静态变量的初始化顺序问题,但不强制编译期求值。
- 通过合理组合这两个关键字,你可以在保证程序安全性与性能的同时,编写更易读、更可靠的 C++20 代码。