Technique for avoiding undefined behavior when accessing a singleton

    The article discusses the causes and methods of avoiding undefined behavior when accessing a singleton in modern c ++. Examples of single-threaded code are provided. Nothing compiler-specific, all in accordance with the standard.

    Introduction


    To begin, I recommend that you read other articles about singleton on Habré:

    Three ages of the Singleton pattern. Singletones
    and common instances
    3 ways to violate the Single Responsibility Principle
    Singleton - pattern or antipattern?
    Using the singleton pattern.

    And, finally, an article that touched on the same topic, but in passing (if only because the flaws and limitations were not considered):
    tialized objects (that is, objects
    Singleton and the lifetime of the object

    Next:

    • this is not an article on singleton's architectural properties;
    • this is not an article “how to make a white and fluffy singleton out of a terrible and terrible singleton”;
    • this is not a singleton campaign;
    • it is not a crusade against singleton;
    • this is not a happy end article.

    This article is about one very important, but still technical aspect of using singleton in modern C ++. The main attention in the article is paid to the moment of destruction of the singleton, as in most sources, the issue of destruction is poorly disclosed. Usually, the emphasis is on the moment the singleton was created, and about destruction, at best, it says something like "destroyed in the reverse order."

    I’ll ask you to follow the scope of the article in the comments, especially not to arrange the singleton pattern vs singleton antipattern holivar.

    So let's go.

    What the standard says


    Quotes are from C ++ 14 final draft N3936, as available C ++ 17 drafts are not marked as “final”.
    I give the most important section in its entirety. Important places are highlighted by me.

    3.6.3 Termination [basic.start.term]

    1. Destructors (12.4) for initialized objects (that is, objects whose lifetime (3.8) has begun) with static storage duration are called as a result of returning from main and as a result of calling std :: exit (18.5). Destructors for initialized objects with thread storage duration within a given thread are called as a result of returning from the initial function of that thread and as a result of that thread calling std :: exit.The completions of the destructors for all initialized objects with thread storage duration within that thread are sequenced before the initiation of the destructors of any object with static storage duration. If the completion of the constructor or dynamic initialization of an object with thread storage duration is sequenced before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first. If the completion of the constructor or dynamic initialization of an object with static storage duration is sequenced before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first.[Note: This definition permits concurrent destruction. –End note] If an object is initialized statically, the object is destroyed in the same order as if the object was dynamically initialized. For an object of array or class type, all subobjects of that object are destroyed before any block-scope object with static storage duration initialized during the construction of the subobjects is destroyed. If the destruction of an object with static or thread storage duration exits via an exception, std :: terminate is called (15.5.1).

    2.If a function contains a block-scope object of static or thread storage duration that has been destroyed and the function is called during the destruction of an object with static or thread storage duration, the program has undefined behavior if the flow of control passes through the definition of the previously destroyed blockscope object. Likewise, the behavior is undefined if the block-scope object is used indirectly (ie, through a pointer) after its destruction.

    3. If the completion of the initialization of an object with static storage duration is sequenced before a call to std :: atexit (see “cstdlib”, 18.5), the call to the function passed to std :: atexit is sequenced before the call to the destructor for the object. If a call to std :: atexit is sequenced before the completion of the initialization of an object with static storage duration, the call to the destructor for the object is sequenced before the call to the function passed to std :: atexit. If a call to std :: atexit is sequenced before another call to std :: atexit, the call to the function passed to the second std :: atexit call is sequenced before the call to the function passed to the first std :: atexit call .

    4. If there is a use of a standard library object or function not permitted within signal handlers (18.10) that does not happen before (1.10) completion of destruction of objects with static storage duration and execution of std :: atexit registered functions (18.5 ), the program has undefined behavior. [Note: If there is a use of an object with static storage duration that does not happen before the object's destruction, the program has undefined behavior. Terminating every thread before a call to std :: exit or the exit from main is sufficient, but not necessary, to satisfy these requirements. These requirements permit thread managers as static-storage-duration objects. —End note]

    5. Calling the function std :: abort () declared in “cstdlib” terminates the program without executing any destructors and without calling the functions passed to std :: atexit () or std :: at_quick_exit ().
    Interpretation:

    • destruction of objects with thread storage duration is performed in the reverse order of their creation;
    • strictly after that, objects with static storage duration are destroyed and calls are made to functions registered with std :: atexit in the reverse order of creating such objects and registering such functions;
    • An attempt to access a destroyed object with thread storage duration or static storage duration contains undefined behavior. Re-initialization of such objects is not provided.

    Note: global variables in the standard are referred to as "non-local variable with static storage duration". As a result, it turns out that all global variables, all singletones (local statics) and all calls to std :: atexit fall into a single LIFO queue as they are created / registered.

    Information useful for the article is also contained in section 3.6.2 Initialization of non-local variables [basic.start.init] . I bring only the most important:
    Dynamic initialization of a non-local variable with static storage duration is either ordered or unordered. [...] Variables with ordered initialization defined within a single translation unit shall be initialized in the order of their definitions in the translation unit.
    Interpretation (taking into account the full text of the section): global variables within one translation unit are initialized in the declaration order.

    What will be in the code


    All code examples provided in the article are published on the github .

    The code consists of three layers, as if written by different people:

    • singleton;
    • utility (class using singleton);
    • user (global variables and main).

    Singleton and the utility are like a third-party library, and the user is the user.
    The utility layer is designed to isolate the user layer from the singleton layer. In the examples, the user has the opportunity to access the singleton, but we will act as if it is impossible.

    The user first does everything right, and then with a flick of the wrist everything breaks. First we try to fix it in the utility layer, and if it does not work out, then in the singleton layer.

    In the code, we will constantly walk along the edge - now on the light side, then on the dark. To make it easier to switch to the dark side, the most difficult case was chosen - accessing a singleton from the utility destructor.

    Why is the case of calling from the destructor the most difficult?Because the utility destructor can be called in the process of minimizing the application, when the question “has the singleton been destroyed or not yet” becomes relevant.

    The case is some kind of synthetic. In practice, calls to a singleton from the destructor are not needed. Even as needed. For example, to log destruction of objects.

    Three classes of singleton are used:

    • SingletonClassic - no smart pointers. In fact, it is not directly quite classical, but definitely the most classical among the three considered;
    • SingletonShared - with std :: shared_ptr;
    • SingletonWeak - with std :: weak_ptr.

    All singletones are templates. The template parameter is used to inherit from it. In most examples, they are parameterized by the Payload class, which provides one public function for adding data to std :: set.

    The utility destructor in most examples tries to fill in a hundred values ​​there. Diagnostic output to the console is also used from the singleton constructor, the singleton destructor, and instance ().

    Why so hard?To make it easier to understand that we are on the dark side. Appeal to the destroyed singleton is an undefined behavior, but it may not be manifested in any way externally. Stuffing values ​​into the destroyed std :: set also certainly does not guarantee external manifestations, but there is no more reliable way (in fact, in GCC under Linux in incorrect examples with the classic singleton, the destroyed std :: set is successfully stuffed, and in MSVS under Windows - hangs). With undefined behavior, output to the console may not happen. So in the correct examples, we expect the absence of access to instance () after the destructor, as well as the absence of a crash and the absence of a hang, and in the incorrect ones, either the presence of such an appeal, or a crash, or a hang, or all at once in any combinations, or whatever.

    Classic singleton


    Payload.h
    #pragma once
    #include 
    class Payload
    {
    public:
      Payload() = default;
      ~Payload() = default;
      Payload(const Payload &) = delete;
      Payload(Payload &&) = delete;
      Payload& operator=(const Payload &) = delete;
      Payload& operator=(Payload &&) = delete;
      void add(int value)
      {
        m_data.emplace(value);
      }
    private:
      std::set m_data;
    };
    


    SingletonClassic.h
    #pragma once
    #include 
    template
    class SingletonClassic : public T
    {
    public:
      ~SingletonClassic()
      {
        std::cout << "~SingletonClassic()" << std::endl;
      }
      SingletonClassic(const SingletonClassic &) = delete;
      SingletonClassic(SingletonClassic &&) = delete;
      SingletonClassic& operator=(const SingletonClassic &) = delete;
      SingletonClassic& operator=(SingletonClassic &&) = delete;
      static SingletonClassic& instance()
      {
        std::cout << "instance()" << std::endl;
        static SingletonClassic inst;
        return inst;
      }
    private:
      SingletonClassic()
      {
        std::cout << "SingletonClassic()" << std::endl;
      }
    };
    


    SingletonClassic example 1


    Classic_Example1_correct.cpp
    #include "SingletonClassic.h"
    #include "Payload.h"
    #include 
    class ClassicSingleThreadedUtility
    {
    public:
      ClassicSingleThreadedUtility()
      {
        // To ensure that singleton will be constucted before utility
        SingletonClassic::instance();
      }
      ~ClassicSingleThreadedUtility()
      {
        auto &instance = SingletonClassic::instance();
        for ( int i = 0; i < 100; ++i )
          instance.add(i);
      }
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    // This guarantee destruction in order:
    // - utilityUnique;
    // - singleton;
    // - emptyUnique.
    // This order is correct
    int main()
    {
      return 0;
    }
    


    Console Output
    instance ()
    SingletonClassic ()
    instance ()
    ~ SingletonClassic ()

    The utility calls the singleton in the constructor to ensure that the singleton is created before the utility is created.

    The user creates two std :: unique_ptr: one empty, the second containing the utility.

    Creation order:

    - empty std :: unique_ptr.
    - singleton;
    - utility.

    And accordingly, the order of destruction:

    - utility;
    - singleton;
    - empty std :: unique_ptr.

    The call from the utility destructor to the singleton is correct.

    SingletonClassic example 2


    Everything is the same, but the user took it and ruined everything with one line.

    Classic_Example2_incorrect.cpp
    #include "SingletonClassic.h"
    #include "Payload.h"
    #include 
    class ClassicSingleThreadedUtility
    {
    public:
      ClassicSingleThreadedUtility()
      {
        // To ensure that singleton will be constucted before utility
        SingletonClassic::instance();
      }
      ~ClassicSingleThreadedUtility()
      {
        auto &instance = SingletonClassic::instance();
        for ( int i = 0; i < 100; ++i )
          instance.add(i);
      }
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    // This guarantee destruction in order:
    // - utilityUnique;
    // - singleton;
    // - emptyUnique.
    // This order seems to be correct ...
    int main()
    {
      // ... but user swaps unique_ptrs
      emptyUnique.swap(utilityUnique);
      // Guaranteed destruction order is still the same:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique,
      // but now utilityUnique is empty, and emptyUnique is filled,
      // so destruction order is incorrect
      return 0;
    }
    


    Console Output
    instance ()
    SingletonClassic ()
    ~ SingletonClassic ()
    instance ()

    The order of creation and destruction is preserved. It would seem that everything is still. But no. By invoking emptyUnique.swap (utilityUnique), the user committed undefined behavior.

    Why did the user do such stupid things? Because he does not know anything about the internal structure of the library, which provided him with a singleton and utility.

    And if you know the internal structure of the library? ... then anyway, in real code it’s very easy to get involved. And you have to get out by painful debag, because to understand what exactly happened will not be easy.

    Why not require the library to be used correctly? Well, there are all kinds of docks to write, examples ... And why not make a library that is not so easy to spoil?

    SingletonClassic example 3


    In the course of preparing the article for several days, I believed that it was impossible to eliminate indefinite behavior from the previous example in the utility layer, and the solution was available only in the singleton layer. But over time, a solution was nevertheless come up.

    Before opening the spoilers with the code and explanation, I suggest the reader to try to find a way out of the situation on their own (only in the utility layer!). I do not exclude that there are better solutions.

    Classic_Example3_correct.cpp
    #include "SingletonClassic.h"
    #include "Payload.h"
    #include 
    #include 
    class ClassicSingleThreadedUtility
    {
    public:
      ClassicSingleThreadedUtility()
      {
        thread_local auto flag_strong = std::make_shared(0);
        m_flag_weak = flag_strong;
        SingletonClassic::instance();
      }
      ~ClassicSingleThreadedUtility()
      {
        if ( !m_flag_weak.expired() )
        {
          auto &instance = SingletonClassic::instance();
          for ( int i = 0; i < 100; ++i )
            instance.add(i);
        }
      }
    private:
      std::weak_ptr m_flag_weak;
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    // This guarantee destruction in order:
    // - utilityUnique;
    // - singleton;
    // - emptyUnique.
    // This order seems to be correct ...
    int main()
    {
      // ... but user swaps unique_ptrs
      emptyUnique.swap(utilityUnique);
      {
        // To demonstrate normal processing before application ends
        auto utility = ClassicSingleThreadedUtility();
      }
      // Guaranteed destruction order is still the same:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique,
      // but now utilityUnique is empty, and emptyUnique is filled,
      // so destruction order is incorrect ...
      // ... but utility uses a variable with thread storage duration to detect thread termination.
      return 0;
    }
    


    Console Output
    instance ()
    SingletonClassic ()
    instance ()
    instance ()
    ~ SingletonClassic ()

    Explanation
    The problem only occurs when minimizing the application. Undefined behavior can be eliminated by teaching the utility to recognize when the application is minimized. To do this, we used a flag_strong variable of the std :: shared_ptr type, which has a thread storage duration qualifier (see excerpts from the standard in the article above) - this is like a static, but only destroyed when the current thread ends before any of the statics are destroyed , including before destruction singleton. The flag_strong variable is one for the entire stream, and each instance of the utility stores its weak copy.

    In a narrow sense, the solution can be called a hack, because it is indirect and non-obvious. In addition, it warns too early, and sometimes (in a multi-threaded application) generally warns false. But in a broad sense, this is not a hack, but a solution with completely defined by the standard properties - both disadvantages and advantages.

    Singletonshared


    Let's move on to a modified singleton based on std :: shared_ptr.

    SingletonShared.h
    #pragma once
    #include 
    #include 
    template
    class SingletonShared : public T
    {
    public:
      ~SingletonShared()
      {
        std::cout << "~SingletonShared()" << std::endl;
      }
      SingletonShared(const SingletonShared &) = delete;
      SingletonShared(SingletonShared &&) = delete;
      SingletonShared& operator=(const SingletonShared &) = delete;
      SingletonShared& operator=(SingletonShared &&) = delete;
      static std::shared_ptr instance()
      {
        std::cout << "instance()" << std::endl;
        // "new" and no std::make_shared because of private c-tor
        static auto inst = std::shared_ptr(new SingletonShared);
        return inst;
      }
    private:
      SingletonShared()
      {
        std::cout << "SingletonShared()" << std::endl;
      }
    };
    


    Ai-ai-ai, the new operator should not be used in modern code, instead std :: make_shared is needed! And this is prevented by the singleton's private constructor.

    Ha! I have a problem too! Declare std :: make_shared a singleton freind! ... and get a variation of the PublicMorozov antipattern: using the same std :: make_shared, it will be possible to create additional instances of the singleton not provided by the architecture.

    SingletonShared Examples 1 and 2


    Fully correspond to examples No. 1 and 2 for the classic version. Significant changes were made only to the singleton layer, the utility essentially remained the same. Just as in the examples with the classic singleton, example-1 is correct, and example-2 shows undefined behavior.

    Shared_Example1_correct.cpp
    #include "SingletonShared.h"
    #include 
    #include 
    class SharedSingleThreadedUtility
    {
    public:
      SharedSingleThreadedUtility()
      {
        // To ensure that singleton will be constucted before utility
        SingletonShared::instance();
      }
      ~SharedSingleThreadedUtility()
      {
        if ( auto instance = SingletonShared::instance() )
          for ( int i = 0; i < 100; ++i )
            instance->add(i);
      }
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    // This guarantee destruction in order:
    // - utilityUnique;
    // - singleton;
    // - emptyUnique.
    // This order is correct
    int main()
    {
      return 0;
    }
    


    Console Output
    instance ()
    SingletonShared ()
    instance ()
    ~ SingletonShared ()

    Shared_Example2_incorrect.cpp
    #include "SingletonShared.h"
    #include "Payload.h"
    #include 
    class SharedSingleThreadedUtility
    {
    public:
      SharedSingleThreadedUtility()
      {
        // To ensure that singleton will be constucted before utility
        SingletonShared::instance();
      }
      ~SharedSingleThreadedUtility()
      {
        // Sometimes this check may result as "false" even for destroyed singleton
        // preventing from visual effects of undefined behaviour ...
        //if ( auto instance = SingletonShared::instance() )
        //  for ( int i = 0; i < 100; ++i )
        //    instance->add(i);
        // ... so this code will demonstrate UB in colour
        auto instance = SingletonShared::instance();
        for ( int i = 0; i < 100; ++i )
          instance->add(i);
      }
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    // This guarantee destruction in order:
    // - utilityUnique;
    // - singleton;
    // - emptyUnique.
    // This order seems to be correct ...
    int main()
    {
      // ... but user swaps unique_ptrs
      emptyUnique.swap(utilityUnique);
      // Guaranteed destruction order is the same:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique,
      // but now utilityUnique is empty, and emptyUnique is filled,
      // so destruction order is incorrect
      return 0;
    }
    


    Console Output
    instance ()
    SingletonShared ()
    ~ SingletonShared ()
    instance ()

    SingletonShared example 3


    And now we’ll try to fix this problem better than in the example number 3 from the classics.
    The solution is obvious: you just need to extend the life of the singleton by storing a copy of std :: shared_ptr returned by the singleton in the utility. And this solution, complete with SingletonShared, has been widely replicated in open sources.

    Shared_Example3_correct.cpp
    #include "SingletonShared.h"
    #include "Payload.h"
    #include 
    class SharedSingleThreadedUtility
    {
    public:
      SharedSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_singleton(SingletonShared::instance())
      {
      }
      ~SharedSingleThreadedUtility()
      {
        // Sometimes this check may result as "false" even for destroyed singleton
        // preventing from visual effects of undefined behaviour ...
        //if ( m_singleton )
        //  for ( int i = 0; i < 100; ++i )
        //    m_singleton->add(i);
        // ... so this code will allow to demonstrate UB in colour
        for ( int i = 0; i < 100; ++i )
          m_singleton->add(i);
      }
    private:
      // A copy of smart pointer, not a reference
      std::shared_ptr> m_singleton;
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of SharedSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    int main()
    {
      // This guarantee destruction in order:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique.
      // This order is correct ...
      // ... but user swaps unique_ptrs
      emptyUnique.swap(utilityUnique);
      // Guaranteed destruction order is the same:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique,
      // but now utilityUnique is empty, and emptyUnique is filled,
      // so destruction order is incorrect...
      // ... but utility have made a copy of shared_ptr when it was available,
      // so it's correct again.
      return 0;
    }
    


    Console Output
    instance ()
    SingletonShared ()
    ~ SingletonShared ()

    And now, attention, the question is: did you really want to extend the life of a singleton?
    Or did you want to get rid of indefinite behavior, and choose life extension as a way lying on the surface?

    Theoretical incorrectness in the form of substitution of goals by means leads to the risk of deadlock (or cyclic reference - call it what you want).

    Yes nuuuuuu, this is how you have to try so hard !? You’ll have to come up with such a long time, and you certainly won’t do it by accident!

    CallbackPayload.h
    #pragma once
    #include 
    class CallbackPayload
    {
    public:
      CallbackPayload() = default;
      ~CallbackPayload() = default;
      CallbackPayload(const CallbackPayload &) = delete;
      CallbackPayload(CallbackPayload &&) = delete;
      CallbackPayload& operator=(const CallbackPayload &) = delete;
      CallbackPayload& operator=(CallbackPayload &&) = delete;
      void setCallback(std::function &&fn)
      {
        m_callbackFn = std::move(fn);
      }
    private:
      std::function m_callbackFn;
    };
    


    SomethingWithVeryImportantDestructor.h
    #pragma once
    #include 
    class SomethingWithVeryImportantDestructor
    {
    public:
      SomethingWithVeryImportantDestructor()
      {
        std::cout << "SomethingWithVeryImportantDestructor()" << std::endl;
      }
      ~SomethingWithVeryImportantDestructor()
      {
        std::cout << "~SomethingWithVeryImportantDestructor()" << std::endl;
      }
      SomethingWithVeryImportantDestructor(const SomethingWithVeryImportantDestructor &) = delete;
      SomethingWithVeryImportantDestructor(SomethingWithVeryImportantDestructor &&) = delete;
      SomethingWithVeryImportantDestructor& operator=(const SomethingWithVeryImportantDestructor &) = delete;
      SomethingWithVeryImportantDestructor& operator=(SomethingWithVeryImportantDestructor &&) = delete;
    };
    


    Shared_Example4_incorrect.cpp
    #include "SingletonShared.h"
    #include "CallbackPayload.h"
    #include "SomethingWithVeryImportantDestructor.h"
    class SharedSingleThreadedUtility
    {
    public:
      SharedSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_singleton(SingletonShared::instance())
      {
        std::cout << "SharedSingleThreadedUtility()" << std::endl;
      }
      ~SharedSingleThreadedUtility()
      {
        std::cout << "~SharedSingleThreadedUtility()" << std::endl;
      }
      void setCallback(std::function &&fn)
      {
        if ( m_singleton )
          m_singleton->setCallback(std::move(fn));
      }
    private:
      // A copy of smart pointer, not a reference
      std::shared_ptr> m_singleton;
    };
    int main()
    {
      auto utility = std::make_shared();
      auto something = std::make_shared();
      // lambda with "utility" and "something" captured
      utility->setCallback( [utility, something](){} );
      return 0;
    }
    


    Console Output
    instance ()
    SingletonShared ()
    SharedSingleThreadedUtility ()
    SomethingWithVeryImportantDestructor ()

    A singleton was created.

    A utility has been created.

    It was created Something-C-very-important-destructors (which I added to intimidate, as the internets found positions such as "well, will not be called the destructor of Singleton, so what of it, it is all the same should be working all the time programs").

    But no destructor was called for any of these objects!

    Because of which? Due to the substitution of goals by means.

    Singletonweak


    SingletonWeak.h
    #pragma once
    #include 
    #include 
    template
    class SingletonWeak : public T
    {
    public:
      ~SingletonWeak()
      {
        std::cout << "~SingletonWeak()" << std::endl;
      }
      SingletonWeak(const SingletonWeak &) = delete;
      SingletonWeak(SingletonWeak &&) = delete;
      SingletonWeak& operator=(const SingletonWeak &) = delete;
      SingletonWeak& operator=(SingletonWeak &&) = delete;
      static std::weak_ptr instance()
      {
        std::cout << "instance()" << std::endl;
        // "new" and no std::make_shared because of private c-tor
        static auto inst = std::shared_ptr(new SingletonWeak);
        return inst;
      }
    private:
      SingletonWeak()
      {
        std::cout << "SingletonWeak()" << std::endl;
      }
    };
    


    Such a modification of the singleton in open sources, if given, is certainly not often. I came across some strange options turned inside out with a std :: weak_ptr, which seems to be used, which, it seems, offer the utility nothing more than to prolong the life of a singleton:


    The option I propose, when applied correctly in singleton and utility layers:

    • protects against actions in the user layer described in the above examples, including prevents deadlock;
    • determines the moment of application folding more accurately than thread_local application in Classic_Example3_correct, i.e. allows you to come closer to the edge;
    • I don’t suffer from the theoretical problem of substituting goals with means (I don’t know if anything tangible other than deadlock can appear from this theoretical problem).

    However, there is a drawback: extending the life of a singleton can still allow it to come even closer to the edge.

    SingletonWeak example 1


    Similar to Shared_Example3_correct.cpp.

    Weak_Example1_correct.cpp
    #include "SingletonWeak.h"
    #include "Payload.h"
    #include 
    class WeakSingleThreadedUtility
    {
    public:
      WeakSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_weak(SingletonWeak::instance())
      {
      }
      ~WeakSingleThreadedUtility()
      {
        // Sometimes this check may result as "false" even in case of incorrect usage,
        // and there's no way to guarantee a demonstration of undefined behaviour in colour
        if ( auto strong = m_weak.lock() )
          for ( int i = 0; i < 100; ++i )
            strong->add(i);
      }
    private:
      // A weak copy of smart pointer, not a reference
      std::weak_ptr> m_weak;
    };
    // 1. Create an empty unique_ptr
    // 2. Create singleton (because of WeakSingleThreadedUtility c-tor)
    // 3. Create utility
    std::unique_ptr emptyUnique;
    auto utilityUnique = std::make_unique();
    int main()
    {
      // This guarantee destruction in order:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique.
      // This order is correct ...
      // ... but user swaps unique_ptrs
      emptyUnique.swap(utilityUnique);
      // Guaranteed destruction order is the same:
      // - utilityUnique;
      // - singleton;
      // - emptyUnique,
      // but now utilityUnique is empty, and emptyUnique is filled,
      // so destruction order is incorrect...
      // ... but utility have made a weak copy of shared_ptr when it was available,
      // so it's correct again.
      return 0;
    }
    


    Console Output
    instance ()
    SingletonWeak ()
    ~ SingletonWeak ()

    Why do we need SingletonWeak, because no one bothers the utility to use SingletonShared as SingletonWeak? Yes, no one bothers. And even no one bothers the utility to use SingletonWeak as SingletonShared. But using them for their intended purpose is slightly easier than using them for other purposes.

    SingletonWeak example 2


    Similar to Shared_Example4_incorrect, but only deadlock does not occur in this case.

    Weak_Example2_correct.cpp
    #include "SingletonWeak.h"
    #include "CallbackPayload.h"
    #include "SomethingWithVeryImportantDestructor.h"
    class WeakSingleThreadedUtility
    {
    public:
      WeakSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_weak(SingletonWeak::instance())
      {
        std::cout << "WeakSingleThreadedUtility()" << std::endl;
      }
      ~WeakSingleThreadedUtility()
      {
        std::cout << "~WeakSingleThreadedUtility()" << std::endl;
      }
      void setCallback(std::function &&fn)
      {
        if ( auto strong = m_weak.lock() )
          strong->setCallback(std::move(fn));
      }
    private:
      // A weak copy of smart pointer, not a reference
      std::weak_ptr> m_weak;
    };
    int main()
    {
      auto utility = std::make_shared();
      auto something = std::make_shared();
      // lambda with "utility" and "something" captured
      utility->setCallback( [utility, something](){} );
      return 0;
    }
    


    Console Output
    instance ()
    SingletonWeak ()
    WeakSingleThreadedUtility ()
    SomethingWithVeryImportantDestructor ()
    ~ SingletonWeak ()
    ~ SomethingWithVeryImportantDestructor ()
    ~ WeakSingleThreadedUtility ()

    Instead of a conclusion


    And what, such a modification of a singleton will eliminate undefined behavior? I promised there would be no happy end. The following examples show that skillful sabotage in the user layer can destroy even the correct thought-out library with a singleton (but we must admit that this can hardly be done by accident).

    Shared_Example5_incorrect.cpp
    #include "SingletonShared.h"
    #include "Payload.h"
    #include 
    #include 
    class SharedSingleThreadedUtility
    {
    public:
      SharedSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_singleton(SingletonShared::instance())
      {
      }
      ~SharedSingleThreadedUtility()
      {
        // Sometimes this check may result as "false" even for destroyed singleton
        // preventing from visual effects of undefined behaviour ...
        //if ( m_singleton )
        //  for ( int i = 0; i < 100; ++i )
        //    m_singleton->add(i);
        // ... so this code will allow to demonstrate UB in colour
        for ( int i = 0; i < 100; ++i )
          m_singleton->add(i);
      }
    private:
      // A copy of smart pointer, not a reference
      std::shared_ptr> m_singleton;
    };
    void cracker()
    {
      SharedSingleThreadedUtility();
    }
    // 1. Register cracker() using std::atexit
    // 2. Create singleton
    // 3. Create utility
    auto reg = [](){ std::atexit(&cracker); return 0; }();
    auto utility = SharedSingleThreadedUtility();
    // This guarantee destruction in order:
    // - utility;
    // - singleton.
    // This order is correct.
    // Additionally, there's a copy of shared_ptr in the class instance...
    // ... but there was std::atexit registered before singleton,
    // so cracker() will be invoked after destruction of utility and singleton.
    // There's second try to create a singleton - and it's incorrect.
    int main()
    {
      return 0;
    }
    


    Console Output
    instance ()
    SingletonShared ()
    ~ SingletonShared ()
    instance ()

    Weak_Example3_incorrect.cpp
    #include "SingletonWeak.h"
    #include "Payload.h"
    #include 
    #include 
    class WeakSingleThreadedUtility
    {
    public:
      WeakSingleThreadedUtility()
          // To ensure that singleton will be constucted before utility
          : m_weak(SingletonWeak::instance())
      {
      }
      ~WeakSingleThreadedUtility()
      {
        // Sometimes this check may result as "false" even in case of incorrect usage,
        // and there's no way to guarantee a demonstration of undefined behaviour in colour
        if ( auto strong = m_weak.lock() )
          for ( int i = 0; i < 100; ++i )
            strong->add(i);
      }
    private:
      // A weak copy of smart pointer, not a reference
      std::weak_ptr> m_weak;
    };
    void cracker()
    {
      WeakSingleThreadedUtility();
    }
    // 1. Register cracker() using std::atexit
    // 2. Create singleton
    // 3. Create utility
    auto reg = [](){ std::atexit(&cracker); return 0; }();
    auto utility = WeakSingleThreadedUtility();
    // This guarantee destruction in order:
    // - utility;
    // - singleton.
    // This order is correct.
    // Additionally, there's a copy of shared_ptr in the class instance...
    // ... but there was std::atexit registered before singleton,
    // so cracker() will be invoked after destruction of utility and singleton.
    // There's second try to create a singleton - and it's incorrect.
    int main()
    {
      return 0;
    }
    


    Console Output
    instance ()
    SingletonWeak ()
    ~ SingletonWeak ()
    instance ()

    Also popular now: