Jua*_*ano 6 c++ metaprogramming static-members compiler-optimization language-lawyer
编译器为什么以及何时优化静态成员变量?我有以下代码
#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; }
Run Code Online (Sandbox Code Playgroud)
在输出中我可以看到Super<A>::m、Super<B>:m、 和C::m都已初始化。
Super<A>::mA a {};如果删除该语句,则不会初始化。这是有道理的,因为 m 永远不会被访问。但这并不能解释为什么 B 和 C 没有被删除。
此行为是指定的还是编译器如何检测未使用的变量的产物?
仅当以需要定义的方式使用时,类模板特化的静态数据成员的定义才会被隐式实例化。
对于您所在的类B,无条件地定义默认构造函数。默认构造函数使用默认构造函数Super<B>来初始化基类,这意味着构造函数的定义Super<B>::Super()将被隐式实例化。此构造函数的定义是 odr-using min 的(void)m;,因此Super<B>::m的定义也将被隐式实例化。
对于 class A,您没有显式定义任何构造函数。仅当以需要定义的方式使用隐式特殊成员函数时,才会定义它们。在该行中,A a {};您正在调用 的隐式默认构造函数A,因此它将被定义。该定义将像以前一样调用 的默认构造函数Super<A>,要求Super<A>::m实例化 的定义。如果没有,A a {};代码中没有任何内容需要定义 的任何特殊成员函数A或 的默认构造函数Super<A>或 的定义m。因此,它们都不会被定义。
在 的情况下C,没有我们需要考虑实例化的模板。C::m被明确定义。
鉴于定义了静态数据成员,它(通常)最终必须被初始化。这里的所有内联静态数据成员都具有动态初始化,具有可观察到的副作用,因此初始化必须在运行时发生。它们是main在 的主体开始执行之前初始化,还是将初始化推迟到内联静态数据成员的第一次非初始化 odr 使用,是由实现定义的。(这是为了允许动态库。)
实际上,您并不是在使用任何内联静态数据成员进行非初始化,因此它们是否实际上会被初始化是由实现定义的。如果实现确实定义了不延迟初始化,则所有已定义的内联静态数据成员也将在main输入之前初始化。
初始化发生的顺序是不确定的。类模板特化的静态数据成员具有无序初始化,这意味着它们与任何其他动态初始化没有顺序保证。并且只有一个静态数据成员不是从模板专门化的,inline因此只有一个是部分排序的,尽管没有其他东西可以与它一起排序。
实际上,还有一个额外的静态存储持续时间对象将在这里初始化,这是一个std::ios_base::Init通过 包含的类型的全局变量<iostream>。该变量的初始化会导致标准流(std::cout等)的初始化。由于模板中的内联静态数据成员具有无序初始化,因此它们不会通过此初始化进行排序。同样,如果您有多个包含 的翻译单元C::m,也不会与它一起订购。因此,您可能会std::cout在初始化之前使用它,从而导致未定义的行为。您可以通过构造类型的对象来提前初始化标准流std::ios_base::Init:
class X {
public:
X(const char* s) {
[[maybe_unused]] std::ios_base::Init ios_base_init;
std::cout << s << "\n";
};
};
Run Code Online (Sandbox Code Playgroud)
除了上述考虑之外,如果静态数据成员的初始化具有可观察到的副作用,则不允许编译器删除它们。当然,as-if 规则仍然一如既往地适用,这意味着编译器可以编译为任何机器指令,这将导致与上述相同的可观察行为。
出于实际目的,您还应该小心。有一些编译器标志有时用于代码大小优化,如果变量似乎未使用,它们将消除动态初始化。(尽管这不是符合标准的行为。)例如,--gc-sections链接器标志与 GCC 一起-ffunction-section -fdata-section可以产生这种效果。
正如您所看到的,静态存储持续时间对象的动态初始化在 C++ 中有点复杂。在您的情况下,只有较小的依赖性问题,但这很快就会变得非常混乱,这就是为什么通常建议尽可能避免它。