C++ constexpr lambda赋值给不同类型变量的初始化问题咨询
首先明确两个constexpr修饰的作用差异:
- lambda表达式末尾的
constexpr修饰的是lambda的调用运算符,仅表明这个lambda的调用过程可以在编译期语境中执行,和持有lambda的变量的初始化时机没有任何关系。 - 变量前加的
constexpr要求变量必须在编译期完成初始化,属于C++的常量初始化范畴,归为静态初始化的一种,初始化结果会直接写入程序的静态数据段,程序加载时就已经生效,不会在运行期执行初始化逻辑。
第一个写法的问题根源
如果squared1是定义在全局/命名空间作用域的静态存储期变量,第一种写法没有给变量加constexpr,编译器不会强制要求编译期初始化,它会被归类为动态初始化:初始化逻辑会在程序启动后、main函数执行前的阶段运行。
而C++标准明确规定,跨不同翻译单元(也就是不同的.cpp文件)的动态初始化顺序是完全未定义的,这就是常说的「静态初始化顺序灾难」的根本来源。如果有另一个翻译单元的全局变量初始化依赖了squared1,就可能在squared1还没完成初始化的时候就调用它,触发未定义行为。
第二个写法的优化逻辑
给变量加了constexpr之后,C++17起无捕获的lambda属于可字面类型,满足常量初始化的要求,squared变量会直接在编译期完成初始化,不存在运行期的初始化步骤,自然就规避了跨翻译单元的初始化顺序依赖问题。
我们可以通过两个跨翻译单元的代码复现问题:
首先是a.cpp:
// a.cpp auto squared1 = [](auto val) constexpr { return val * val; };
然后是b.cpp:
// b.cpp #include <iostream> extern decltype([](auto v)constexpr{return v*v;}) squared1; // 声明a.cpp中定义的squared1 // 全局变量初始化依赖squared1的返回值 const int init_val = squared1(5); // 预期结果为25 int main() { std::cout << init_val << std::endl; return 0; }
上述代码编译运行时,因为两个翻译单元的动态初始化顺序不确定,b.cpp里的init_val初始化时,a.cpp的squared1可能还没完成初始化,调用未初始化的lambda的调用运算符属于未定义行为,可能输出随机值、程序崩溃,甚至出现其他不符合预期的结果。
注意:如果两个全局变量定义在同一个翻译单元,动态初始化顺序是按照定义先后执行的,问题不会暴露,但跨翻译单元的场景下风险始终存在。
如果把a.cpp中的squared1的定义改为加constexpr前缀的写法,squared1会在编译期完成初始化,init_val的初始化也可以在编译期完成,运行时会稳定输出25,不存在任何顺序问题。
内容的提问来源于stack exchange,提问作者Denis

