跳转至

consteval 与 constinit

标准 难度 状态 分级 证据 审阅 最后校验
C++20 3/5 final A 3 条(manual 3) 单审(zyuhao7) 2026-10-26

编译器支持:GCC 11+ · Clang 17+ · MSVC 19.29+。版本查询:cppreference 编译器支持。

主张与证据

主张 支持证据
用 consteval 在编译期计算目标平台的分支距离上限 ev-mold-consteval-max-distance-001
用特性测试宏把兼容宏映射到标准 constinit,保证静态存储期变量常量初始化 ev-abseil-constinit-macro-002
用 consteval 构造函数在编译期校验格式串 ev-fmt-consteval-format-string-003

本站证据页:ev-abseil-constinit-macro-002 · ev-fmt-consteval-format-string-003 · ev-mold-consteval-max-distance-001

解决什么问题

constexpr 函数有两种执行方式:实参是常量表达式时可以在编译期求值,否则在运行期执行。库作者想强制某个函数只在编译期运行,过去只能靠约定、static_assert 技巧或编译器扩展。

静态存储期变量还有另一类问题:动态初始化发生在 main 之前,跨翻译单元的顺序未指定,容易触发“静态初始化顺序灾难”。constexpr 变量能保证常量初始化,但也强制变量为 const,无法表达“初始化必须是常量,之后可以修改”。

C++20 通过 P1073R3 引入 consteval,通过 P1143R2 引入 constinit。consteval 声明立即函数,所有调用都必须在常量求值中完成;constinit 要求静态或线程存储期变量使用常量初始化,但不要求变量本身是 const。

基础用法

#include <iostream>

consteval int square(int value) { return value * value; }

constinit int global_counter = square(4);

int main() {
  global_counter += 1;
  std::cout << global_counter << ' ' << square(5) << '\n';
}

square 只能以常量表达式调用;把运行期变量传进去会直接编译失败。global_counter 由 square(4) 常量初始化,因此在进入 main 前已经确定,不存在动态初始化顺序问题;它仍然可以在运行时递增。

进阶用法

consteval 也可以标记构造函数,把“参数必须能在编译期校验”写进类型。格式库的编译期格式串类型就是这一模式:构造函数是立即函数,运行期字符串无法构造出该类型。

struct Token {
  int value;
  consteval explicit Token(int input) : value(square(input)) {}
};

Token token{4};        // 可以:实参是常量表达式
static_assert(square(5) == 25);

Token token{4} 在编译期完成校验和计算;传入运行期变量会直接编译失败。这样类型本身就表达了“已校验”的不变式,调用方不需要额外运行时检查。

constinit 与 constexpr 的差别在可变性:constinit int counter = square(3); 保证常量初始化,但 counter 之后仍可修改。它适用于静态存储期和线程存储期变量,包括块作用域的 static;自动变量不能标 constinit。

完整示例见 examples/cpp20/consteval_constinit.cpp。

真实项目证据

项目 位置 用法 为什么典型
rui314/mold src/thunks.cc#L36-L43 用 consteval 计算目标平台的分支距离上限 目标架构在编译期已知,结果参与代码生成,不应在运行时重复计算
abseil-cpp absl/base/attributes.h#L842-L847 用 __cpp_constinit 检测并映射到标准 constinit 兼容层在可用时直接使用标准关键字,明确静态初始化保证
fmt include/fmt/base.h#L2698-L2709 用 consteval 构造函数在编译期校验格式串 立即函数保证格式串检查只发生在常量求值中,运行期字符串无法绕过

三条证据均为 manual 级:作者复核,尚未引入第二名审核者。它们覆盖 consteval 的两种生产用法与 constinit 的跨标准落地方式。

陷阱与边界

consteval 不能接收运行期输入。 std::cin >> n; square(n); 直接编译失败,即使该分支在运行时不会执行也一样。需要运行期计算时用 constexpr 函数,而不是把 consteval 当作普通函数。

立即函数不能延迟到运行期调用。 取 consteval 函数地址并保存为函数指针通常直接被拒绝;需要运行期多态时改用普通函数或 constexpr 函数。

constinit 只适用于静态或线程存储期变量。 函数内的自动变量不能标 constinit;局部变量需要常量初始化时直接用 constexpr 或普通初始化。

constinit 不保证变量不可变。 它只约束初始化方式;变量之后仍可被修改,多线程写入仍需同步。把 constinit 当成 const 会引入数据竞争。

初始化表达式必须是常量表达式。 依赖运行期配置、堆分配或非 constexpr 构造函数的初始值不能用 constinit;编译器的诊断会指出哪一部分不是常量。

constexpr 变量已经是常量初始化。 对 constexpr 变量不能再叠加 constinit,标准规定两者最多只能出现一个;需要可变全局量时才用 constinit。

静态初始化顺序问题没有完全消失。 constinit 只保证该变量自身没有动态初始化;其他仍走动态初始化的全局对象依旧有顺序问题,跨翻译单元的依赖要显式设计。

编译器支持要用特性测试宏判断。 使用 __cpp_consteval 与 __cpp_constinit 做条件编译;不同编译器对 consteval 的完整支持版本差异较大,不能只按语言标准开关判断。

库联动

  • mold:用 consteval 计算目标架构的分支距离上限,结果参与代码生成,避免运行期重复计算。
  • fmt:FMT_CONSTEVAL 在 C++20 下展开为 consteval,格式串构造函数借此完成编译期语法与类型检查;旧编译器则退化为普通构造函数加运行期检查。
  • Abseil:ABSL_CONST_INIT 在支持时映射到标准 constinit,让静态存储期变量的初始化方式在兼容层中保持一致。

横向对比

写法 求值时机 变量可变 常量初始化保证 主要用途
constexpr 函数 编译期或运行期 — — 两种场景复用同一实现
consteval 函数 仅编译期 — — 强制立即求值的编译期计算
constinit 变量 初始化必须编译期 是 是 避免静态初始化顺序问题,保留可变性
constexpr 变量 编译期 否 是 编译期常量
函数内 static 首次调用时 是 否 惰性初始化;C++11 起线程安全
显式初始化函数 + 断言 运行期 是 否 需要运行期依赖的全局状态

跨标准演进:C++11 引入 constexpr;C++20 增加 consteval 与 constinit;C++23 扩展 constexpr 对部分标准库类型的支持,但不会改变 consteval 的强制编译期语义。constinit 与 consteval 解决的是不同问题:一个约束变量初始化,一个约束函数调用时机。