Search code examples
c++constructordestructor

Destructor is elided when constructor is absent?


The code below reproduces the issue I have, MSVC 2022:

#include <iostream>

struct A {
    static void message()
    {
        std::cout << "A::message()\n";
    }

    struct B {
        B() {}
        ~B()
        {
            A::message();
        }
    };

    static inline B b;
};
 
int main()
{
}

With the above code I see, as expected, the message appearing. However:

  1. If I remove the constructor B(), then the destructor ~B() seems to be elided because the message disappears.
  2. If I then add a dummy property int dummy; to struct B then the message appears again.

Questions I really could not find an answer on:

  1. It looks to me that without the constructor B() and no properties in B, the compiler simply labels class B as empty and just skips it entirely. Is this correct?
  2. Is it somewhere documented that this can occur? Given the contents of the destructor I would think this should never happen.
  3. How can I guarantee that it will always work correctly, not only for the compiler I'm using right now. Is defining the constructor B() enough, or should I also make B non-empty with the dummy property int dummy;?

Solution

  • The difference lies in whether or not b has static initialization.

    If a static storage duration variable has static initialization, then it is initialized before any other static storage duration variable without static initialization (i.e. dynamic initialization).

    In particular, there is one other important static storage duration object defined in <iostream>, namely an instance of std::ios_base::Init. The initialization of an object of that type causes the initialization of the standard IO streams (e.g. std::cout). It is also responsible for flushing all of these streams when the last instance of it is destroyed.

    So, in order to see your output you must make sure that the std::ios_base::Init object is destoyed after your b. However, the order of destruction is in reverse of the order of initialization. So, if b has static initialization but the std::ios_base::Init object has dynamic initialization, then it is the wrong way around. If b does not have static initialization, then it will be ordered correctly, ONLY if b's definition exists in every translation unit that includes (directly or indirectly) <iostream>, after the first such inclusion. This is because you used inline, which makes the dynamic initialization of b only partially-ordered.

    Generally, if the initialization of a static storage duration variable is a constant expression, then the variable has static initialization. That's the case here if you don't declare any constructor.

    If the initialization is not a constant expression, then it usually has dynamic initialization, except that, as long as it wouldn't change the resulting values of static storage duration variables after their initialization, the implementation is free to choose static initialization instead for any such variable. If you use a non-constexpr constructor or leave a member uninitialized, then you have this situation.

    So, to reliably get the order correct, you need to assure that b does not have static initialization. Because of the leeway given to the implementation, this is a bit tricky. Doing something in B's constructor that obviously can only happen at runtime should be sufficient (e.g. calling some IO), but the way the standard currently specifies it, it is difficult to be completely sure because differences in side effects are not considered. See open CWG issue 1294.

    A better alternative may be to add a std::ios_base::Init object as member to B, so that one such object is surely kept alive while b's destructor is running.

    Also, as you can see above construction and destruction of static storage duration variables in C++ is pretty complicated. If you can, it would be much more straight-forward to simply declare b as a non-static local variable in main. There generally isn't any guarantee that the static inline variable would be constructed before the first non-initialization odr-use from main anyway (that's implementation-defined).


    Please note that I didn't verify that this is actually the cause in the case of MSVC. The answer is based on theoretical considerations of the standard and what an implementation could do.

    Also, I am using my interpretation of [basic.start.term]/3, which I read to mean that the order of destruction of statically initialized variables is ordered with the dynamic initialized variables according to their actual order of initialization. Another interpretation that I could see in the wording is that the order of destruction is such as if all variables had been dynamically initialized according to the reverse initialization order rules for dynamic initialization. Unfortunately the text doesn't seem clear to me and at least GCC and Clang seem to follow the latter interpretation. In that case you wouldn't see your problem.