6

Consider the following class:

template <class T>
struct Test
{
    Test() {
        if (!f) {
            f = []() { std::cout << "it works\n"; };
            initialized = true;
        }
    }
    static void check() {
        if (f) f();
        else std::cout << "f is empty\n";
    }
    T x{};
    inline static std::function<void()> f;
    inline static bool initialized = false;
};

An object of such class, if declared globally (Yes, I know it's a bad practice to use global variables), seems to empty the function f after it is initalized. The following example demonstrates this:

Test<double> t;

int main()
{
    t.x = 1.5;
    std::cout << "t.x = " << t.x << "\n";
    std::cout << std::boolalpha << "initalized: " <<  Test<double>::initialized << "\n";
    Test<double>::check();
    return 0;
}

This prints:

t.x = 1.5
initalized: true
f is empty

I expected Test<double>::check(); to print it works.

However, the above example works as expected, when I do either of the following:

  • declare t within main()
  • do not use template for Test (just replace T with double for example)
  • make f and check() be not static
  • use plain function pointer instead of std::function

Does anyone know why this happens?

miloszmaki
  • 1,635
  • 15
  • 21
  • 1
    @miloszmaki It works with clang but does not work with gcc.:) – Vlad from Moscow Jul 23 '22 at 11:09
  • Interesting :) I was using gcc. – miloszmaki Jul 23 '22 at 11:11
  • You didn't initialize it. You do assignment in constructor. Which might play games with caching\optimization. Note, the assignment would happen every time object is created, which defeats purpose of static member. – Swift - Friday Pie Jul 23 '22 at 12:15
  • @Swift-FridayPie Right, I meant assignment not initialization. However, the assignment happens only once because of `if (!f) { f = ... }` condition. – miloszmaki Jul 26 '22 at 10:01
  • @miloszmaki and you are creating possible weak point because that's not an atomic operation. Why do that if language got exactly same mechanics properly implemented natively and atomicly? I mean, that would be static member initialization. – Swift - Friday Pie Jul 26 '22 at 16:57
  • @Swift-FridayPie Agreed. Well, I presented here a simplified example for the sake of question's clarity. Originally I had a more complex code. – miloszmaki Jul 27 '22 at 08:09

3 Answers3

8

The problem is related to the order of initialization of static variables which I guess is solved differently for the templated instantiated static variables compared to Test<double> on different compilers.

inline static Holder f;

is a static variable, so somewhere before entering main it will be default initialized (to an empty function). But Test<double> is another static variable that will get its own initialization before entering main.

On GCC it happens that

  • Test<double> is called
  • Test<double>::f is set by the constructor of Test<double>
  • the default constructor of Test<double>::f is called, thus emptying the function

This all happens inside __static_initialization_and_destruction_0 GCC method, if you actually use a wrapper object to break on static initialization of the variable you can see what's happening: https://onlinegdb.com/UYEJ0hbgg

How could the compiler know that you plan to set a static variable from another static variable before its construction? That's why, as you said, using static variables is a bad practice indeed.

Jack
  • 131,802
  • 30
  • 241
  • 343
  • Specifically the dynamic initialization of static data members which were instantiated from a template is completely unordered with any other dynamic instantiation. And even worse, it is implementation-defined whether the initialization of an `inline` static data member is deferred until its first non-initialization odr-use which would here happen only when `check` is called. – user17732522 Jul 23 '22 at 13:01
1

I found a possible solution to my problem. Instead of using a static variable f, one can define a static function creating the necessary static variable when used for the first time:

template <class T>
struct Test
{
    Test() {
        auto &f = getFunction();
        if (!f) f = []() { std::cout << "it works\n"; };
    }
    static void check() {
        auto &f = getFunction();
        if (f) f();
        else std::cout << "f is empty\n";
    }
    static std::function<void()>& getFunction() {
        static std::function<void()> f;
        return f;
    }
    //...
};
miloszmaki
  • 1,635
  • 15
  • 21
-4

When you call

 Test<double>::initialized

and

Test<double>::check();

the constructor Test() was not called, hence the undefined content of both variables initialized and f

What you might wanna do is probably calling them

t.check();
t.initialized;
  • 4
    The constructor was called when the global t variable was initialized before entering main. – Wutz Jul 23 '22 at 12:19