C++编译器何时会优化移除静态成员变量?
以下是对应测试代码:
#include <iostream> #include <typeinfo> class X { public: X(const char* s) { std::cout << s << "\n"; }; }; template <class S> class Super { protected: Super() { (void)m; }; static inline X m { typeid(S).name() }; }; class A : Super<A> { }; class B : Super<B> { B() {}; }; class C { static inline X m { "c" }; }; A a {}; int main() { return 0; }
编译器所有优化都必须遵守as-if原则:不能改变程序的可观测行为。只有当标准明确允许不执行某段逻辑,或者某段逻辑完全没有可观测副作用时,编译器才可以将其消除。上述代码中三类静态成员的不同表现,一部分是C++标准的明确规定,一部分是编译器实现的保守处理结果:
C::m始终被初始化是标准强制要求的C是普通非模板类,它的inline静态成员m不属于模板实体,不存在“按需实例化”的规则。只要类C的定义出现在翻译单元中,这个静态成员就是程序静态存储期的正式对象,必须在程序启动阶段完成动态初始化。它的构造函数有明确的打印输出(可观测副作用),编译器没有任何依据可以消除这个初始化流程,否则就违反了标准要求。移除
A a{};后Super<A>::m不初始化是符合标准的预期行为
模板类中定义的inline静态成员属于模板实体,标准明确规定这类实体只有在被*odr-use(单一定义规则层面的使用)*时,编译器才需要实例化对应的代码、执行初始化。
当全局对象a的定义被移除后,程序中没有任何地方需要构造A类型的对象:A没有自定义构造函数,它的隐式默认构造函数从未被调用,也就不会触发基类Super<A>构造函数的实例化。而Super<A>::m唯一的使用点就在Super的构造函数中,既然构造函数本身都没有被实例化,自然不会构成对m的odr-use,编译器完全可以不生成m的初始化代码,这个行为完全符合C++标准。Super<B>::m被初始化是编译器实现的保守处理结果,不属于标准强制要求
按照严格的标准规则,B的构造函数是私有的,没有任何友元声明,程序中不存在任何可以调用该构造函数的位置,也就是说B的构造函数、Super<B>的构造函数、Super<B>::m整条链路都没有被odr-use,理论上Super<B>::m和移除全局对象后的Super<A>::m一样,可以不被初始化。
但实际编译器在处理模板实例化、尤其是CRTP这类递归模板模式时,为了降低分析复杂度、避免漏实例化导致的链接错误,往往不会做深度的可达性分析:只要类B的定义出现,就会保守地实例化直接基类Super<B>的构造函数,进而触发构造函数中对m的引用,最终生成m的初始化代码。这个行为是编译器实现选择带来的副产物,在不同编译器、不同优化等级下可能出现不同表现。
内容的提问来源于stack exchange,提问作者Juan I Carrano

