54 / 80 · C++20 · 约 9 分钟
constexpr、consteval 与 constinit 各管什么
constexpr 表达常量求值的能力与变量约束,consteval 要求立即调用满足常量表达式条件,constinit 约束静态或线程存储期变量的初始化。三者并不等价,尤其 constinit 不会使变量只读。
constexpr:可在常量上下文使用
C++11 的 constexpr 变量要求合适的常量初始化,并使对象为 const;constexpr 函数则提供进入常量求值的能力,但普通调用不一定必须在翻译期间求值。把结果放进 constexpr 变量或 static_assert,才明确建立本次调用的常量表达式要求。
从 C++14 起函数体规则逐步放宽,循环和局部变量等写法得以用于常量计算,但仍需按目标标准检查限制。constexpr 不是“永远更快”的注解,编译器本来也可以优化普通表达式;它首先提供语言层面的可验证契约,而不是性能测量结论。
consteval:调用必须满足立即求值规则
C++20 的 consteval 声明立即函数,普通潜在求值调用必须满足常量表达式要求,不能把只有运行期才知道的输入直接传入。它适合编译期校验、固定表生成等确实不允许退回运行期的接口,而不是所有小函数的默认修饰。
示例的 square 同时支持常量与运行期语境,而 fixed_limit 明确是立即函数。两者都不会因为词法上出现在 main 外就自动获得正确结果;决定性因素是函数声明、实参与调用上下文。对外提供工具时,应先决定是否真的需要禁止运行期调用。
constinit:管初始化,不管后续修改
C++20 的 constinit 用于静态或线程存储期变量,要求不存在动态初始化。它可把意外引入的运行期初始化变成编译错误,帮助控制跨翻译单元的初始化依赖,却不会让之后的读写自动线程安全,也不会让变量自动变成 const。
示例让全局 counter 以常量方式初始化,然后在 main 中递增,证明 constinit 与只读是两回事。若既要常量值又不允许修改,constexpr 更合适;若需要可变全局状态而只想锁定初始化阶段,可考虑 constinit,但优先评估是否能减少全局共享状态。
容易答错的地方
- constexpr 函数遇到运行期实参仍可正常执行;不能把函数带 constexpr 当作每次调用都完成了编译期计算的证据。
- constinit 不是 const,不约束后续写入,也不提供同步;多线程修改仍要使用适当的锁或原子对象。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
constexpr int square(int x) { return x * x; }
consteval int fixed_limit() { return square(4); }
constinit int counter = square(2);
int main() {
constexpr int limit = fixed_limit();
static_assert(limit == 16);
int input = 3;
const int result = square(input);
++counter;
assert(result == 9 && counter == 5);
std::cout << limit << ' ' << result << ' ' << counter << '\n';
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread modern-constexpr.cpp -o example && ./example预期结果
16 9 5
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为什么 constinit int local = 4 不能直接作为 main 中普通自动变量,而 static constinit int local = 4 可以?
查看参考答案
constinit 约束静态或线程存储期初始化,普通块内自动变量不属于该范围。增加 static 后变量获得静态存储期,并以常量 4 初始化,满足要求。它仍能在之后赋值;若只是需要块内不可变常量,应直接写 constexpr int local = 4。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。