c++staticc++17global-variablesstatic-order-fiasco

Is accessing a global `static` variable from a single TU guaranteed to be safe?


The SFML library initializes an audio device when the first object of type AudioResource is created, and deinitializes it when the last one is destroyed.

I am trying to simplify that code, as I believe that the current solution using std::[shared/weak]_ptr is overkill.

I proposed an alternative using a static global variable. Here's a simplified version (the real one is protected by std::mutex):

// AudioResource.hpp --------------------------------

struct AudioResource
{
     AudioResource();
    ~AudioResource();
    // ...
};
// AudioResource.cpp --------------------------------

#include "AudioResource.hpp"

static int deviceRC = 0;

 AudioResource() { if (deviceRC++ == 0) { /* init device   */ }; }
~AudioResource() { if (--deviceRC == 0) { /* deinit device */ }; }
// ...

I'm think the code above is OK and not subject to static order initialization fiasco, as the deviceRC global static is defined and accessed only in one TU.

My understanding is that -- as long as there are no dependencies between multiple global static variables from different TUs -- there's no risk.

Am I correct, or is it possible that the code above results in undefined behavior by somehow accessing deviceRC before it is initialized?

Is there any scenario where creating AudioResource objects from multiple TUs or when using SFML as a shared library can cause issues?

Is the code safe even if deviceRC does not get constant-initialized? E.g.,

// SomeOtherTU.cpp --------------------------------

static AudioResource audioResource; 
    // Could `deviceRC` be accessed while uninitialized here?

Other SFML maintainers have suggested using a function-scope static variable here:

// AudioResource.cpp --------------------------------

#include "AudioResource.hpp"

static int& deviceRC() 
{
    static int result = 0;
    return result;
}

 AudioResource() { if (deviceRC()++ == 0) { /* init device   */ }; }
~AudioResource() { if (--deviceRC() == 0) { /* deinit device */ }; }
// ...

Is the approach using a fuction-scope static variable safer and/or necessary compared to the one using a file-scope static variable?


Solution

  • If deviceRc is statically initialized, the code is safe because:

    If deviceRc is not statically initialized, then you have a problem. audioResource could indeed be initialized before deviceRc, since the two variables are defined in different TUs. When audioResource is being initialized, it's possible that it could see the value deviceRc has prior to dynamic initialization, that is, zero. If deviceRc has dynamic initialization, then presumably the initialization is more complicated than just setting it to zero. So, presumably, the program is not correct if deviceRc's value is read before that dynamic initialization is complete.

    The strategy with the function-local static guarantees that result is initialized before it is accessed, and result is destroyed after any static variable that could have accessed result during its own construction. These guarantees will normally ensure that your code doesn't have any initialization or destruction order fiascos, though you can create one if you really try.