ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

constexpr / constinit / consteval:编译期常量三兄弟怎么选

constexpr / constinit / consteval:编译期常量三兄弟怎么选 const不等于「编译期就能确定」而 C20 一口气给了三个长得像的兄弟constexpr、constinit、consteval。它们都跟「编译期」有关但职责各不相同——用错一个要么编译失败要么埋下跨翻译单元的初始化顺序雷。这篇把三者拆开并用真实输出证明它们各自做了什么。官方文档constexpr、constinit、consteval1. 引子const 不一定能在编译期用很多人以为「加了const就是编译期常量」结果在需要常量表达式的地方翻车intreadConfig(){return42;}voidbad(){constintruntime_constreadConfig();// const但初值是运行时值// int arr[runtime_const] {}; // 反例不要这么写runtime_const 不是常量表达式编译失败(void)runtime_const;}const只保证「运行时不让改」不保证「编译期就已知」。要编译期常量得靠constexpr。2. constexpr编译期常量也可能只是 constconstexpr变量 必须是常量表达式初始化 编译期就确定能当数组大小、模板实参。constexpr函数则承诺「至少存在一组实参能在编译期求值」。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostreamconstexprintkConstexpr10;// 编译期常量constintkConst20;// 本例初值也是常量但 constexpr 才是显式承诺intmain(){intarr1[kConstexpr]{};// ✓ constexpr 可作数组大小intarr2[kConst]{};// ✓ 初值是常量表达式时也可以std::coutkConstexpr kConstexpr\n;std::coutkConst kConst\n;}kConstexpr 10 kConst 20关键区别constexpr强制初值是常量表达式const没这个强制。下面这段代码编译不过正好说明边界// 展示片段无 main不进校验下面这行若真去编译会直接报错正说明 constexpr 的边界intreadConfig(){return42;}constexprintbadreadConfig();// 反例不要这么写readConfig() 不是常量表达式编译失败3. constevalC20强制编译期求值consteval函数是「立即函数immediate function」——任何调用都必须在编译期完成连一次运行时调用都不允许。它比constexpr更狠constexpr还能在运行时调用consteval不行。// verify: stdc20#includeiostreamconstevalintsquare(intx){returnx*x;}// 必须编译期求值intmain(){constexprintrsquare(5);// 编译期算出来intarr[square(4)]{};// 直接当数组大小用std::coutsquare(5) r\n;std::cout数组大小来自 square(4) (int)(sizeof(arr)/sizeof(arr[0]))\n;}square(5) 25 数组大小来自 square(4) 16若把square(y)传一个运行时变量y编译器直接拒绝——这正是consteval的价值把「必须在编译期算」变成硬性合同。4. constinitC20保证静态初始化constinit只保证「这个变量走静态初始化static initialization而不是动态初始化」。它不是 const——允许后续修改只是初始化那一刻必须在编译期完成。它专门用来治一个老大难问题静态初始化顺序陷阱。// verify: stdc20#includeiostreamconstexprintmakeValue(){return7;}constinitintg_staticmakeValue();// 保证静态初始化编译期完成// int g_dynamic makeValue(); // 对比可能走动态初始化存在顺序隐患intmain(){std::coutg_static g_static\n;g_static8;// constinit 不是 const可以改std::cout改之后 g_static g_static\n;}g_static 7 改之后 g_static 85. Static Initialization Order FiascoSIOF到底是什么跨翻译单元不同.cpp的命名空间级静态对象初始化顺序标准未定义。若 A 的初始化依赖 B而 B 还没初始化就会读到「零值 / 野值」。这就是经典的Static Initialization Order Fiasco静态初始化顺序陷阱。// 两个 .cpp谁先初始化由链接顺序决定标准不保证 // ---- a.cpp ---- ---- b.cpp ---- extern int beta; extern int alpha; int alpha beta 1; int beta alpha 1; // ↑ 若 b 先于 a 初始化alpha 还是 0 beta1错 问题根因beta / alpha 是「动态初始化」初值不是纯编译期常量 解法用 constinit / constexpr 让它变成「静态初始化」编译期就定好不依赖别的 TUconstinit强制该变量在静态初始化阶段main 之前、且先于任何动态初始化就定型从而不再依赖别的 TU 的初始化顺序——这正是它存在的意义。官方文档isocpp FAQ静态初始化顺序 给出了用「函数内 static 局部变量」的另一种解法Meyers Singleton。6. 三兄弟选型表特性能否编译期求值变量初值要求变量后续能否改主要用途constexpr是变量必须常量表达式否隐含 const编译期常量、编译期可算的函数constinit是初始化时常量表达式能不是 const保证静态初始化治 SIOFconsteval强制调用即编译期常量表达式函数无状态必须编译期算的元函数 / 类型计算一句话选法要一个编译期就定值、且不该被改的常量 -constexpr要一个跨 TU 的全局状态、且绝不能在别的 TU 之前偷偷动态初始化-constinit要一个函数 100% 必须在编译期算不允许任何运行时调用 -consteval7. 完整示例三兄弟同台把三者放进一个程序覆盖前面的所有要点需要 C20// verify: stdc20#includeiostreamconstevalintsquare(intx){returnx*x;}// 编译期必算constexprintkN3;// 编译期常量constinitintg_counter0;// 静态初始化编译期之后可变intmain(){intarr[square(kN)]{};// consteval 当数组大小std::coutsquare(3) square(3)\n;std::cout数组大小 (int)(sizeof(arr)/sizeof(arr[0]))\n;g_counter1;// constinit 允许改std::coutg_counter g_counter\n;}square(3) 9 数组大小 9 g_counter 18. 性能与内存视角编译期求值省掉了什么编译期求值不是语法糖它省掉的是运行时的真实工作常量折叠让square(6)的结果直接以立即数写进机器码运行时一次乘法都不剩constexpr/constinit的全局变量在链接期就把初值写进.rodata/.data进程启动时没有任何代码需要执行。反过来动态初始化的全局对象会在.init_array里登记一个构造函数每次启动都被调用一遍——程序里有几百个这样的对象时这份启动开销是实打实的。动态初始化的全局对象初值来自函数调用 .init_array ── 启动时逐个调用构造函数 ── 每次进程启动都要付这份钱 constexpr / constinit 全局变量 .rodata / .data ── 链接期就写死初值 ── 启动时零代码要执行不过constexpr函数不保证编译期求值它只是「允许」// verify: stdc20#includeiostreamconstexprintsquare(intx){returnx*x;}// 两栖编译期、运行时都能调用constevalintcube(intx){returnx*x*x;}// 只能是编译期intmain(){constexprintfoldedsquare(6);// 实参是常量 编译期算完只剩立即数 36intn6;intruntimesquare(n);// 实参是运行时值 老老实实生成一次乘法std::cout编译期 square(6) folded\n;std::cout运行时 square(6) runtime\n;std::coutconsteval cube(3) cube(3)\n;}编译期 square(6) 36 运行时 square(6) 36 consteval cube(3) 27同一个square装在constexpr变量里被折叠成常数喂一个运行时变量就退化成普通函数调用——这正是constexpr和consteval的分界线前者「两栖」后者只许编译期。9. 易错点四个常见误解误解一constexpr变量一定占一块内存。不一定。它只在被 ODR-use取地址、绑引用等时才需要落一个存储位置像constexpr int kN 3;这种只用在arr[kN]里的通常完全不占运行时内存。误解二constinit也能加在局部变量上。不能它只约束静态存储期或线程存储期的变量// 反例不要这么写局部变量是自动存储期加 constinit 会编译失败voidf(){constinitintx1;}误解三consteval只是「更强的 constexpr」可以当普通函数到处用。取它的地址、或者把运行时值传进去都会直接编译失败——因为每个调用点都必须能在编译期算出结果。误解四函数加了constexpr就一定更快。只有调用点真的处在常量上下文里才享受折叠实参是运行时值的话生成的代码和普通函数几乎没有区别。反过来说constexpr的正确姿势就是「能加就加」运行期零成本却能把一部分工作提前到编译期做完编译不过的还能当场报错。10. 延伸阅读constexpr — cppreference变量 / 函数 / if constexpr 的完整规则constinit — cppreference明确写「只约束初始化」consteval — cppreference立即函数的硬约束isocpp FAQSIOF跨 TU 初始化顺序陷阱与解法Compiler Explorer看constexpr是否被真的常量折叠进机器码11. 一句话总结constexpr给「编译期定值且不可改」的常量constinitC20只保证初始化走静态初始化、之后可变专治静态初始化顺序陷阱constevalC20把函数锁死成「调用即编译期求值」。选法要常量用constexpr要全局状态不踩 SIOF 用constinit要函数严禁运行时调用用consteval。
返回列表