In-Depth C++ Singleton Pattern: Principles, Implementation Comparisons, and shared_ptr Architecture Design

#cpp

#singleton

πŸ“Œ In-Depth C++ Singleton Pattern: Principles, Implementation Comparisons, and shared_ptr Architecture Design

1️⃣ What is a Singleton? Basic Semantics and Usage Scenarios

A Singleton ([[Singleton]]) is one of the most common object creation patterns, aimed at ensuring that a class has only one instance in the system and provides a global access point.

βœ… Common Usage Scenarios:

  • Configuration Center (ConfigManager)
  • Logging System (Logger)
  • Resource Pool (e.g., memory pool, thread pool)
  • Scheduler, Session Manager
  • Framework Registry, Plugin System

2️⃣ Five Common Implementations of Singleton

Recommended Usage:

  1. Lazy Initialization + Thread-Safe (C++11 Standard) Local Static Variable (Meyers Singleton) β€” 【Recommended】
  2. shared_ptr Singleton (more flexible)

πŸƒ1. Eager Initialization Singleton (Early Instantiation)

class Singleton {
public:
    static Singleton& instance() {
        return inst;
    }
private:
    Singleton() {}
    static Singleton inst;
};
Singleton Singleton::inst;
  • βœ… Simple and easy to understand, thread-safe (ensured by C++ static object initialization)
  • ❌ Resource may be wasted, instance constructed at program startup

πŸƒ2. Lazy Initialization + Non-Thread-Safe

class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance_)
            instance_ = new Singleton;
        return instance_;
    }
private:
    Singleton() {}
    static Singleton* instance_;
};
Singleton* Singleton::instance_ = nullptr;
  • βœ… Instance created only when needed
  • ❌ Not thread-safe, multiple threads may construct multiple instances simultaneously

πŸƒ3. Lazy Initialization + Thread-Safe (C++11 Standard) Local Static Variable (Meyers Singleton) β€” 【Recommended】

class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;  // C++11 guarantees thread safety
        return inst;
    }
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  • βœ… Recommended: thread-safe, lazy-loaded, no lock overhead
  • βœ… Automatic management of destruction, relies on C++ static local variable characteristics
  • ❗ Copy construction and assignment are prohibited to prevent instance copying
  • βœ… The simplest, safest, and most efficient method (default recommendation)

Later, there will be a syntax semantics and underlying source code analysis of this method

πŸƒ4. Lazy Initialization + Double-Checked Locking (DCLP)

class Singleton {
public:
    static Singleton* instance() {
        if (!inst) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!inst) inst = new Singleton;
        }
        return inst;
    }
private:
    static Singleton* inst;
    static std::mutex mtx;
};
  • βœ… Lazy loading, suitable for scenarios requiring dynamic construction and destruction control
  • ❌ Complex implementation, prone to errors
  • ⚠️ Easy to make mistakes in implementations before C++11 (lack of memory barriers)

πŸƒ5. Smart Pointer + Lazy Construction

class Singleton {
public:
    static std::shared_ptr<Singleton> instance() {
        static std::shared_ptr<Singleton> inst(new Singleton);
        return inst;
    }
};

Supports custom destruction, more suitable for resource lifecycle management

πŸƒ6. Template-based shared_ptr Singleton (Advanced) β€” The Right Way to Use Modern C++

template<typename T>
class SharedSingleton {
public:
    using Ptr = std::shared_ptr<T>;

    static void setDeleter(std::function<void(T*)> deleter) {
        getDeleter() = std::move(deleter);
    }

    template<typename... Args>
    static Ptr instance(Args&&... args) {
        std::call_once(getOnceFlag(), [&] {
            instance_() = Ptr(new T(std::forward<Args>(args)...), getDeleter());
        });
        return instance_();
    }

    static void reset() {
        std::lock_guard<std::mutex> lock(getMutex());
        instance_().reset();
        getOnceFlag() = std::once_flag();
    }

private:
    static Ptr&& instance_() {
        static Ptr inst;
        return inst;
    }

    static std::once_flag&& getOnceFlag() {
        static std::once_flag flag;
        return flag;
    }

    static std::mutex&& getMutex() {
        static std::mutex mtx;
        return mtx;
    }

    static std::function<void(T*)>&& getDeleter() {
        static std::function<void(T*)> deleter = [](T* p) { delete p; };
        return deleter;
    }
};

3️⃣ Implementation Comparison Analysis

Implementation Construction Control Destruction Control Supports Parameters Thread-Safe Lifecycle Control Engineering Recommendation
Eager Initialization ❌ ❌ ❌ βœ… ❌ 🟑
Lazy Initialization + DCLP βœ… ❌ ❌ ⚠️ ❌ ❌ (High Risk)
Meyers Singleton βœ… ❌ ❌ βœ… (C++11) ❌ βœ…
shared_ptr Static βœ… βœ… ❌ βœ… Partial βœ…
shared_ptr Template Encapsulation βœ… βœ… βœ… βœ… βœ… βœ…βœ…βœ…

4️⃣ Lazy Initialization + Thread-Safe (C++11 Standard) Local Static Variable (Meyers Singleton) β€” Syntax Semantics and Underlying Source Code Analysis

πŸƒ Thread Safety of Local Static Variables (C++11)

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

C++11 Standard <span>[stmt.dcl/4]</span><span>: [[Initialization of Local Static Variables]] is [[Thread-Safe]].</span>

The [[compiler]] locks before initialization, ensuring that only one thread can construct it.

The compiler generates equivalent pseudo-code (illustrative):

Singleton& getInstance() {
    static bool initialized = false;
    static char storage[sizeof(Singleton)];
    static std::once_flag flag;

    std::call_once(flag, [&] {
        new (&storage) Singleton();
        initialized = true;
    });

    return *reinterpret_cast<Singleton*>(&storage);
}

πŸƒ Destruction Order and Static Object Lifecycle Issues

  • Local <span>static</span> instances will be automatically [[destructed]] after exiting <span>main()</span>
  • If you want to manually control the lifecycle (e.g., still exist after <span>exit()</span>), you need to use <span>new</span> with <span>atexit</span> or <span>shared_ptr</span><span> implementation.</span>

πŸƒ Source Code Level Analysis (glibc + clang libc++)

At the [[glibc]] level:<span>pthread_once</span><span> implementation</span>

In [[glibc]], <span>std::call_once</span><span> relies on </span><code><span>pthread_once</span><span>:</span>

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

<span>pthread_once</span> uses locks + state flags + [[memory barrier]] to ensure initialization occurs only once, which is the basis for [[C++11]] <span>std::call_once</span>.

[[libc++]] implementation (<span><mutex></span><span>)</span>

namespace std {
template<class Callable>
void call_once(once_flag&& flag, Callable&& f);
}

Internally, it uses a double state atomic marker (<span>__called</span><span>, </span><code><span>__complete</span><span>)</span>

In the GCC implementation, it uses <span>__atomic_load_n</span><span> + </span><code><span>__sync_bool_compare_and_swap</span><span> to ensure [[atomicity]].</span>

5️⃣ Underlying Principle Analysis: Semantics + ABI Behavior + Construction Order

🧡 <span>std::call_once</span><span> + </span><code><span>std::once_flag</span>

static std::once_flag f;
std::call_once(f, [] { singleton = new Singleton(); });

πŸ”₯ Memory Model Analysis

  • Local static: stored in static area
  • shared_ptr: heap resources, released by reference counting

6️⃣ ResourcePool / ThreadPool Engineering Practice

auto pool = SharedSingleton<BufferPool>::instance(32, 8*1024);
auto buf = pool->acquire();
auto pool = SharedSingleton<ThreadPool>::instance(8);
pool->enqueue([] { do_work(); });

βœ… Summary and Design Philosophy

Capability Meyers Singleton shared_ptr Encapsulation
Safety βœ… βœ…
Flexibility ❌ βœ…βœ…
Lifecycle Control ❌ βœ…βœ…βœ…
Test Friendliness ❌ βœ…βœ…
Engineering Adaptability 🟑 βœ…βœ…βœ…

Leave a Comment