Singleton mode (C + + implementation of lazy)

Posted by twmcmahan on Thu, 30 Dec 2021 20:02:29 +0100

Singleton pattern is one of design patterns. This paper records and summarizes the definition of singleton pattern and several lazy ways to implement singleton pattern in C + +.

reference resources Summary and analysis of C + + single case mode

What is singleton mode

Single case The characteristic of (Singleton) mode is that this class has only one instance object globally, and this unique instance can be accessed at all locations through the interface provided by this class. The most classic usage scenario is public cache, which is written periodically by one thread and read by multiple threads. At this time, the Singleton mode can be used to ensure that different threads write and read the same instance .

There are several basic points for C + + to realize the singleton mode:

  1. Ensure that there is only one instance in the global. Setting the constructor to private prevents users from defining instances themselves.
  2. The user must obtain the instance through a static member function GetInstance. Static member functions can be called without defining an object.
  3. Assignment and copying of instances should be prohibited.
  4. Ensure thread safety.

Lazy single case

Hungry vs lazy

reference resources Lazy singleton and hungry singleton

Hungry man means that no matter whether someone calls getInstance to get an instance or not, they will create the instance in advance. They are very diligent and hungry. Hungry Chinese style can be realized by adding a static member variable to the class (note that the static member variable of C + + must be initialized outside the class. The following reference code is java code, so don't worry about this problem)

public class One {
    //The privatization construction method makes it impossible to instantiate through external new
    private One(){}
    //Prepare a class attribute that points to an instantiated object. Because it is a class attribute, there is only one
    private static One instance = new One();

    public static One getInstance(){
        return instance;
    }
}

Lazy means that the instance is created only when someone calls getInstance to get the instance. It can be seen that it is lazy. The advantage is that it will not occupy memory when no one calls.

The most basic lazy (thread unsafe, memory unsafe)

#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak

class Singleton{
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton* m_instance_ptr;
public:
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    static Singleton* get_instance(){
        if(m_instance_ptr==nullptr){
              m_instance_ptr = new Singleton;
        }
        return m_instance_ptr;
    }
    void use() const { std::cout << "in use" << std::endl; }
};

Singleton* Singleton::m_instance_ptr = nullptr;

int main(){
    Singleton* instance = Singleton::get_instance();
    Singleton* instance_2 = Singleton::get_instance();
    return 0;
}

As you can see, assignment and copy functions are prohibited, and the constructor is set to private, only through the static member function get_instance to get the instance. However, the consideration here is not comprehensive enough:

  • Thread unsafe: when multiple threads call get concurrently_ Instance, it is considered that m_instance_ptr==nullptr, resulting in the creation of multiple instances, which violates the singleton principle.
  • Memory unsafe: only new, not delete

Smart pointer and lock to achieve lazy (thread safety, memory safety)

Implementation of intelligent pointer and lock

#include <iostream>
#include <memory> // shared_ptr
#include <mutex>  // mutex

// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak

class Singleton{
public:
    typedef std::shared_ptr<Singleton> Ptr;
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Ptr get_instance(){

        // "double checked lock"
        if(m_instance_ptr==nullptr){
            std::lock_guard<std::mutex> lk(m_mutex);
            if(m_instance_ptr == nullptr){
              m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_instance_ptr;
    }


private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
    static Ptr m_instance_ptr;
    static std::mutex m_mutex;
};

// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;

int main(){
    Singleton::Ptr instance = Singleton::get_instance();
    Singleton::Ptr instance2 = Singleton::get_instance();
    return 0;
}

Improvements are made to the previous basic implementation version:

  • Share with smart pointer_ PTR creates an instance. When no pointer points to this instance, shared_ptr will automatically destroy the instance and reclaim its occupied space.
  • Lock the instance to ensure that only one instance will be created. Moreover, the idea of double check lock is used here. First judge whether it is null, and then add the lock to avoid blocking the normal acquisition of instances in the case of existing instances because the locking operation is placed outside.

Lazy implementation of local static variables (thread safety)

#include <iostream>

class Singleton
{
public:
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton& get_instance(){
        static Singleton instance;
        return instance;

    }
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
};

int main(int argc, char *argv[])
{
    Singleton& instance_1 = Singleton::get_instance();
    Singleton& instance_2 = Singleton::get_instance();
    return 0;
}


By getting_ Static variables are introduced into the instance function to ensure thread safety and memory safety without using smart pointers and locks,

  • In C++11 standard Magic Static This feature ensures thread safety. If a variable enters the declaration statement simultaneously during initialization, the concurrent thread will block and wait for the end of initialization.
  • No pointer is used to the new object, and there is no memory leak problem.

Because static local variables are initialized for the first time when the program executes to the declaration of the object, this is also a lazy type. (hungry Chinese style uses the static member variable of the class, which does not belong to this class.)

reference resources The usage of static in C/C + + global and local variables Lifetime of C + + static variables

Topics: C++ Singleton pattern