静态成员变量何时被优化掉?

6

编译器何时会优化掉静态成员变量?以下是我的代码:

#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; }

我可以看到的输出是,Super<A>::mSuper<B>:mC::m都已经被初始化。

如果删除语句A a {};Super<A>::m就不会被初始化。这是有意义的,因为m从未被访问。然而,这并不能解释为什么对于B和C没有被删除。

这种行为是指定的还是编译器检测未使用变量的产物?


没问题,你可以在这里试用各种编译器:https://godbolt.org/z/GvxMPzTGY,我猜测这也可能与链接时优化有关(https://llvm.org/docs/LinkTimeOptimization.html)。 - Pepijn Kramer
为什么这么复杂,还有构造函数和其他的东西?在编译器的汇编输出中,一个 int 成员也同样容易看到。可能编译器不允许省略已存在对象的构造函数调用;允许在某些情况下跳过复制构造函数的规则是特定于复制构造的。你的测试案例正在寻找可见副作用(cout <<)的变化,而不仅仅是优化一些未被引用的存储。 - Peter Cordes
@PeterCordes 我使用了副作用,因为这段代码是某些具有副作用的东西的最小测试案例。 - Juan I Carrano
哦,所以你只关心可观察行为的变化,而不是仅仅优化一下只被构造一次但在此之后从未被读取(或写入)的static int成员的存储?那么应该加上[language-lawyer]标签,因为C++标准必须明确允许这种优化,而不是通过as-if规则。 - Peter Cordes
1
我正在实现这个的变体:https://dev59.com/u5Pea4cB1Zd3GeqP_SMh - Juan I Carrano
1
我的看法是 "编译器检测未使用变量的一种方法的产物". 需要注意的是,CSuper 是不同的情况,因为模板遵循不同的排序规则。 - Goswin von Brederlow
1个回答

6
类模板特化的静态数据成员的定义仅在以需要定义的方式使用时才会隐式实例化。
对于类B,您无条件地定义了默认构造函数。默认构造函数使用Super的默认构造函数来初始化基类,这意味着将隐式实例化Super::Super()构造函数的定义。这个构造函数的定义在(void)m;中使用了m,并且因此也将隐式实例化Super::m的定义。
在类A的情况下,您没有显式定义任何构造函数。隐含的特殊成员函数仅在以需要定义的方式使用时才被定义。在行A a {};中,您调用了A的隐含默认构造函数,因此它将被定义。该定义将像之前一样调用Super的默认构造函数,需要实例化Super::m的定义。如果没有A a {};,则代码中没有任何东西需要定义A的任何特殊成员函数,Super的默认构造函数或m的定义。因此,它们都不会被定义。
对于C,没有模板需要考虑实例化。C::m是显式定义的。
考虑到静态数据成员已定义,必须(通常)最终对其进行初始化。所有内联静态数据成员均具有动态初始化和可观察副作用,因此初始化必须在运行时发生。初始化是在main函数主体开始执行之前进行还是延迟到内联静态数据成员的第一个非初始化odr-use时,这是实现定义的。(这旨在允许动态库。)
您实际上没有使用任何内联静态数据成员的非初始化odr-use,因此实现可能会定义初始化不被延迟,因此所有已定义的内联静态数据成员将在进入main函数之前初始化。
初始化的顺序是不确定的。类模板特化的静态数据成员具有无序初始化,这意味着它们与任何其他动态初始化没有排序保证。只有一个静态数据成员未从模板专门化,并且该静态数据成员是内联的,因此仅部分排序,尽管它可以与其他任何东西排序。
实际上,这里会初始化一个额外的静态存储期对象,它是一个全局变量,类型为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";
    };
};

除了上述的考虑因素外,如果静态数据成员的初始化具有可观察的副作用,编译器是不允许删除它们的。当然,像往常一样,仍然适用于as-if规则,这意味着编译器可以编译成任何机器指令,以产生与上述描述相同的可观察行为。
为了实际应用,您还应该小心。有一些编译器标志有时用于代码大小优化,如果变量似乎未使用,则会消除动态初始化。(虽然这不符合标准行为。)例如,--gc-sections链接器标志与GCC的-ffunction-section -fdata-section组合使用可以产生此效果。
正如您所看到的,在C++中,静态存储期对象的动态初始化有点复杂。在这种情况下,您只有少量的依赖问题,但这很快就会变得非常混乱,这就是为什么通常建议尽可能避免使用它的原因。

1
我也遇到了初始化顺序未定义的问题,最终我使用了一个静态方法,该方法返回在内部定义的静态变量的引用。至于编译器删除该方法,我使用了__attribute__((used)),这似乎解决了问题。 - Juan I Carrano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接