1
头图

Singleton Pattern

Review the singleton model, and consider implementing a common singleton template class to achieve the goal of low code on the business side.

Prologue

Which is the most common pattern in the design pattern? There will hardly be any differences, it must be a one-piece model. The so-called singleton mode is a technology developed after experiencing various global variables out of control in the history of C language development. Thanks to the encapsulation ability of C++, we can combine various global variables The control is in a global static class (or the realization of static variables in a class) to prevent the catastrophic consequences of arbitrarily placing global variables.

I don't need to repeat here how bad these consequences can be, because this article is not an introductory textbook, but a summary of practical experience.

Obviously, the static class is just the beginning, it is not a particularly good solution, because in addition to the advantage of being able to concentrate in one place, these static variables are still predestined, in addition, the early (before C++11) The compiler does not have a clear and unified agreement on the initialization order of static variables, so it is more difficult for you to deal with the initialization timing of these variables. Another problem is that there is no means for you to implement lazy loading of these variables (lazyinit), unless you put them all Become a pointer. In that case, only God knows how many things will happen.

Theoretical basis

Singleton pattern is one of Creational Patterns . In talk about the Factory mode , we have already introduced the creation mode, so this article will not repeat it.

The intent of the one-piece mode and its basic realization are very simple, so you don't have to spend pen and ink to make up the number of words, just skip it.

Goal

What we want is a thread-safe singleton model that can be lazyinit or can control the timing of initialization.

So the following will introduce a number of Singleton implementations that can be used since C++11 (including C++0x), but skip the earlier implementation methods, and skip the example implementations that are reckless. Well, one of them is like this:

class singleton {
  static singleton* _ptr;
  singleton() {}
  public:
  singleton* get() {
    if (_ptr == NULL)
      _ptr = new singleton();
    return _ptr;
  }
};

This implementation is the most people-friendly, because anyone can write by hand without any knowledge (you still need to know C++), and you don't even need to worry about hand errors or other compilation errors. Its weaknesses, the more obvious ones, will not be mentioned first. Sometimes an important weakness that will be ignored or concealed intentionally or unintentionally by the presenter is that _ptr will not be deleted: but users will convince themselves that my program is like this A pointer leak, the price is affordable.

Terrible? Maybe not. This is true.

Those who pay attention to a little bit know that C provides a means of atexit, so they will try to mount a delete routine when exiting, but it still can't solve the cross-thread data racing problem that may occur here if(_ptr==null) The problem is that if you put a C technique in a C++er, it is also very impure, isn't it?

So, the following text begins.

Meyers' Singleton in C++

Scott Meyers is the author of the Effective C++ series. He first provided a concise version of the Singletion model:

static Singleton& instance() {
  static Singleton instance;
  return instance;
}

"This approach is founded on C++'s guarantee that local static objects are initialized when the object's definition is first encountered during a call to that function." ... "As a bonus, if you never call a function emulating a non-local static object, you never incur the cost of constructing and destructing the object."

—— Scott Meyers

The Singleton model has been circulating for many years, and there are many ways to achieve different purposes, but Meyers' version is the most refined and thread-safe, it is completely practical.

Backstage

C++11 Initialization 1613963a34b481 for the static variables in the function to ensure that the static variables can be uniquely constructed and destroyed under the premise of satisfying thread-safety. Regarding ensuring the thread safety of static variable initialization and destruction is subdivided into multiple types, C++11 guarantees orderliness, and C++17 supports Partially-ordered dynamic initialization . Of course, there is no need to dig out such detailed technical specifications. instance() for the instance variable in 0613963a34b47a above, in fact, the compiler will introduce a hidden variable for this purpose to help identify whether it is initialized:

static bool __guard = false;
static char __storage[sizeof(Singleton)]; // also align it

Singleton& Instance() {
  if (!__guard ) {
    __guard = true;
    new (__storage) Singleton();
  }
  return *reinterpret_cast<Singleton*>(__storage);
}

// called automatically when the process exits
void __destruct() {
  if (__guard)
    reinterpret_cast<Singleton*>(__storage)->~Singleton();
}

Due to the existence of optimization, the compiler often omits __guard and directly uses the instance static variable, because the variable is a pointer in the assembly, and non-zero can be used to represent its bool state. Therefore, taking x86-64 clang 9 as an example, the assembly code generated by Meyers singleton is as follows ( godbolt ):

singleton_t::instance():            # @singleton_t::instance()
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        cmp     byte ptr [guard variable for singleton_t::instance()::instance], 0
        jne     .LBB1_4
        movabs  rdi, offset guard variable for singleton_t::instance()::instance
        call    __cxa_guard_acquire
        cmp     eax, 0
        je      .LBB1_4
        mov     edi, offset singleton_t::instance()::instance
        call    singleton_t::singleton_t() [base object constructor]
        jmp     .LBB1_3
.LBB1_3:
        movabs  rax, offset singleton_t::~singleton_t() [base object destructor]
        mov     rdi, rax
        movabs  rsi, offset singleton_t::instance()::instance
        movabs  rdx, offset __dso_handle
        call    __cxa_atexit
        movabs  rdi, offset guard variable for singleton_t::instance()::instance
        mov     dword ptr [rbp - 16], eax # 4-byte Spill
        call    __cxa_guard_release
.LBB1_4:
        movabs  rax, offset singleton_t::instance()::instance
        add     rsp, 16
        pop     rbp
        ret

Please note that __cxa_guard_acquire directly affects instance, as __cxa_guard_release .

Source Code

A complete Mayers' Singleton Pattern implementation using C++11 looks like this:

#include <stdio.h>

struct singleton_t {
  static
  singleton_t &instance() {
    static singleton_t instance;
    return instance;
  } // instance

  singleton_t(const singleton_t &) = delete;
  singleton_t & operator = (const singleton_t &) = delete;

private:
  singleton_t() {}
  ~singleton_t() {}

public:
  void out(){ printf("out\n"); }
}; // struct singleton_t

int main() {
    singleton_t::instance().out();
    return 0;
}

Basically, you copy it, change the class name, and you can start. This is the practice of most class GlobalVar.

More implementation method reference

For more discussion about C++ Singleton Pattern, see here .

As for those who do double-check or something else, they are all scum. Because of the thread safety of Singleton since C++11, no additional coding considerations are required.

Templated implementation

Mayers' version is perfect, concise and concise, but there is a problem, that single instance is always initialized before main() starts, we cannot do lazyinit.

What is the use of lazyinit?

If we intend to maintain a number of singleton classes, and many of them ctor() are more expensive, then lazyinit can reduce the startup time, and can avoid those singleton classes that are never used at runtime and never have to be constructed. Instance.

This requires changing the static instance to a unique_ptr.

Standard implementation

In hicc-cxx / cmdr-cxx , we provide a templated singleton<T> , which is very helpful for saving code and also supports thread-safe lazyinit:

namespace hicc::util {

  template<typename T>
  class singleton {
    public:
    static T &instance();

    singleton(const singleton &) = delete;
    singleton &operator=(const singleton) = delete;

    protected:
    struct token {};
    singleton() = default;
  };

  template<typename T>
  inline T &singleton<T>::instance() {
    static const std::unique_ptr<T> instance{new T{token{}}};
    return *instance;
  }

} // namespace hicc::util

The problem that the make_unique_ptr/make_shared_ptr of the C++11 standard library cannot work on private constructors has been discussed by many parties, but the direct solution is not concise (and it is difficult to achieve cross-compiler compatibility), so in singleton<T> provides a struct token method to deny users directly constructing a class-in order to allow derived classes to implement special constructors, struct token {} and ctor() are marked as protected.

Generally speaking, the use of this template class must be in the form of a derived class, but requires a special constructor:

#include <hicc/hz-common.hh>
// #include <cmdr11/cmdr_common.hh>

class MyVars: public hicc::util::singleton<MyVars> {
  public:
  explicit MyVars(typename hicc::util::singleton<MyVars>::token) {}
  long var1;
};

int main() {
  printf("%ld\n", MyVars.instance().var1);
}

If you don't care about the encoding prevention of manual instantiation, you can simplify the writing of derived classes, but you need to remove the use of tokens in template classes.

Variable parameter Singleton template

If your class needs to construct parameters, the problem is a little more complicated. You can use our singleton_with_optional_construction_args , which is also directly supported from C++11, without hacking:

namespace hicc::util {

  template<typename C, typename... Args>
  class singleton_with_optional_construction_args {
    private:
    singleton_with_optional_construction_args() = default;
    static C *_instance;

    public:
    ~singleton_with_optional_construction_args() {
      delete _instance;
      _instance = nullptr;
    }
    static C &instance(Args... args) {
      if (_instance == nullptr)
        _instance = new C(args...);
      return *_instance;
    }
  };

  template<typename C, typename... Args>
  C *singleton_with_optional_construction_args<C, Args...>::_instance = nullptr;

} // namespace hicc::util

The usage method is roughly like this:

void test_singleton_with_optional_construction_args() {
  int &i = singleton_with_optional_construction_args<int, int>::instance(1);
  UTEST_CHECK(i == 1);

  tester1 &t1 = singleton_with_optional_construction_args<tester1, int>::instance(1);
  UTEST_CHECK(t1.result() == 1);

  tester2 &t2 = singleton_with_optional_construction_args<tester2, int, int>::instance(1, 2);
  UTEST_CHECK(t2.result() == 3);
}

The original source of this realization is untestable.

It is also not an optimal implementation.

In fact, it can be rewritten singleton<T> , no need to use new and delete, but to tell the truth, I have never used a singleton class with construction parameters, so there is no motivation to rewrite.

Stay here, just to provide a reference template, I will not recommend you to directly implement it in the project, unless you can improve it yourself.

Epilogue

Strictly speaking, the above templated implementation only needs C++11 support. But considering that I have already written a text about c++ design patterns, and I decided not to care about the characteristics of c++11 (an old topic, it is not as attractive as 17), so I still crown it Forget the title name.

Topic 2: About the initialization before main

In the Turbo C era, the execution of an executable file initiated from the OS (such as DOS/Linux via EXE/PE/ELF) before main was completed through c0.asm. After receiving the OS transfer to the code execution control, it completes the preparation of the C environment, completes the registration of the callback function registered by _atexit, and then transfers the execution control to _main. During this period, we can specify _DATA , _BSS , etc.), and control the initialization priority order of variables by specifying the compilation order.

Later, in the era of C++, taking Visual C++ as an example, in addition to completing the preparation of the C environment, you also need to prepare the CRT library (c0crt.asm). The CRT library of VC is to a large extent its standard library. For gcc, it may be libstdc++ or the like. During this period, some non-standard extensions even allow us to specify that a certain static variable can be initialized before the CRT library.

After C++11, the initialization work before main is roughly divided into three parts: basic C environment, stdc library, stdc++ library. If necessary, you can replace these core libraries. If you are trying to write an OS kernel, then You usually have to replace them. As the syntax and semantic capabilities of C++ itself have been further standardized and enhanced, there is no longer any or it is recommended that you use non-standard extensions, we no longer think about whether the design of a singleton should be prior to stdc/stdc++. Is initialized.

Having said that, you may not understand why it is necessary to initialize points in advance, but in fact, there are. If you want to take over some function entries of stdc, or you need a special logging support, etc., it will indeed be used. On such a weird trick. However, this is indeed not the norm, nor will it be applied in large-scale production.

But let's stop here, because writing singleton a long time ago was really an era of barbaric growth, so I will briefly talk about these topics.

:end:

If the text is messy, view the original text


hedzr
95 声望19 粉丝