简体   繁体   中英

When are static member variables optimized away?

Why and when does the compiler optimize away static member variables? I have the following code

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

On the output I can see that Super<A>::m , Super<B>:m , and C::m are all initialized.

Super<A>::m is not initialized if the statement A a {}; is removed. It makes sense because then m is never accessed. However this does not explain why it is not removed for B and C.

Is this behavior specified or is it an artifact of how the compiler detects unused variables?

The definition of a static data member of a class template specialization is implicitly instantiated only if it is used in such a way that a definition would be required.

For the class B you are, unconditionally, defining the default constructor. The default constructor uses the default constructor of Super<B> to initialize the base, meaning that the definition of the Super<B>::Super() constructor will be implicitly instantiated. This constructor's definition is odr-using m in (void)m; and therefore Super<B>::m 's definition will also be implicitly instantiated.

In the case of class A , you are not explicitly defining any constructor. The implicit special member functions will be defined only when they are used in such a way that a definition would be required. In the line A a {}; you are calling the implicit default constructor of A and hence it will be defined. The definition will be calling the default constructor of Super<A> as before, requiring the Super<A>::m 's definition to be instantiated. Without A a {}; there is nothing in the code requiring a definition of any special member function of A or the default constructor of Super<A> or the definition of m . Therefore none of them will be defined.

In the case of C , there is no template for which we would need to consider instantiation. C::m is explicitly defined.


Given that the static data member is defined, it must (generally) be initialized eventually. All of the inline static data members here have dynamic initialization with observable side effects, so the initialization must happen at runtime. It is implementation-defined whether they will be initialized before main 's body starts execution or whether initialization will be deferred upto the first non-initialization odr-use of the inline static data member. (This is meant to allow for dynamic libraries.)

You aren't actually non-initialization odr-using any of the inline static data members, so it is implementation-defined whether they will actually be initialized at all. If the implementation does define the initialization to not be deferred, then all of these inline static data members which have been defined will also be initialized before main is entered.

The order in which the initializations will happen is indeterminate. The static data members of the class template specializations have unordered initialization , meaning they have no ordering guarantees with any other dynamic initialization. And there is only one static data member which isn't specialized from a template and that one is inline and therefore only partially ordered , although there is nothing else it could be ordered with.

Actually, there is one additional static storage duration object which will be initialized here, a global variable of type std::ios_base::Init included through <iostream> . The initialization of this variable causes the initialization of the standard streams ( std::cout , etc.). Because your inline static data members from the templates have unordered initialization, they will not be ordered with this initialization. Similarly if you had multiple translation units containing C::m , it would also not be ordered with it. As a consequence you might be using std::cout before it is initialized, causing undefined behavior. You can cause early initialization of the standard streams by constructing an object of type std::ios_base::Init :

class X {
public:
    X(const char* s) {
        [[maybe_unused]] std::ios_base::Init ios_base_init;
        std::cout << s << "\n";
    };
};

Aside from considerations such as above, the compiler is not allowed to remove static data members if their initialization has observable side effects. Of course the as-if rule still applies as always meaning that the compiler can compile to whatever machine instructions which will result in the same observable behavior as described above.


For practical purposes you should also be careful. There are some compiler flags that are sometimes used for code size optimization which will eliminate dynamic initialization if the variable seems to be unused. (Although that is not standard-conforming behavior.) For example the --gc-sections linker flag together with GCC's -ffunction-section -fdata-section can have this effect.


As you can see dynamic initialization of static storage duration objects is kind of complicated in C++. In your case here there are only minor dependency issues, but this can quickly become very messy, which is why it is usually recommended to avoid it as much as possible.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM