static member std::function of template class gets empty despite initalization
Asked Answered
S

3

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?

Streaming answered 23/7, 2022 at 10:58 Comment(6)
@Streaming It works with clang but does not work with gcc.:)Push
Interesting :) I was using gcc.Streaming
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.Quill
@Swift-FridayPie Right, I meant assignment not initialization. However, the assignment happens only once because of if (!f) { f = ... } condition.Streaming
@Streaming 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.Quill
@Swift-FridayPie Agreed. Well, I presented here a simplified example for the sake of question's clarity. Originally I had a more complex code.Streaming
P
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.

Predikant answered 23/7, 2022 at 11:51 Comment(1)
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.Gono
S
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;
    }
    //...
};
Streaming answered 23/7, 2022 at 15:13 Comment(1)
That's a singleton\factory approach and it may have some overhead (of checking status of local static variable)Quill
A
-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;
Adila answered 23/7, 2022 at 11:53 Comment(1)
The constructor was called when the global t variable was initialized before entering main.Coexecutor

© 2022 - 2024 — McMap. All rights reserved.