2026/01/16 / Coding

GoF Design Patterns - Singleton

A practical guide to the Singleton pattern from the GoF collection, with C++ examples, thread-safety notes, usages, anti-patterns, and mermaid diagrams.

Coding DesignPattern

Hello! I'm Pan-kun.

This article covers the Singleton pattern from the GoF (Gang of Four) design patterns series.
It provides practical guidance including C++ example code, how to implement it, when to use it, important caveats, and visualizations using mermaid diagrams.

Introduction

The Singleton pattern ensures that a class has only one instance within an application and provides a global access point to that single instance.

Key aspects that typically define a Singleton implementation:

  • Private constructor: prevents direct instantiation from outside.
  • Static instance: the class holds the single instance.
  • Static accessor: a function to provide access to the single instance.

Wikipedia defines it as:

"In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one 'single' instance."
(Source: https://en.wikipedia.org/wiki/Singleton_pattern)

From the GoF perspective, the pattern centralizes control of object creation and access, which is useful for shared resources such as configuration, loggers, or connection pools.

Article goals:

  • Show representative C++ implementation patterns and their pros/cons
  • Explain thread-safe initialization, performance, and testability concerns
  • Outline alternatives (e.g., Dependency Injection) and anti-patterns

Primary references (selected):

These references serve as the basis for the implementations and design guidance below.

Typical Use Cases

  • Centralized configuration management
  • Shared logger instance
  • Hardware resources or connection pools where a single manager instance suffices
  • Application-wide caches or factories

Because a Singleton creates a global instance, it can make testing harder. Before adopting it, always ask: "Is it really necessary?" and "Is this the single instance that should be globally accessible?"

C++ Implementation Patterns

Several implementation strategies are common for the Singleton pattern:

  • Eager initialization
  • Lazy initialization
  • Double-checked locking (DCL)
  • Initialization-on-demand holder (Meyers' Singleton style)

Below are a few C++ examples simplified for learning.

Eager Singleton (simple)

Eager singleton is straightforward. In this approach the instance is created at program startup (static initialization). That means the creation cost happens during startup, and you may face static initialization order issues in complex programs.

WebSite/content/other/blog_00028.en.md#L1-120
// EagerSingleton.h
#pragma once
#include <iostream>

class EagerSingleton {
private:
    static EagerSingleton instance; // created at program startup
    EagerSingleton() = default;
public:
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

    static EagerSingleton& GetInstance() {
        return instance;
    }

    void Say() { std::cout << "EagerSingleton\n"; }
};

// EagerSingleton.cpp
EagerSingleton EagerSingleton::instance; // definition required

Lazy Singleton with mutex

A lazy singleton delays instance creation until first use. The example below uses a mutex to ensure thread safety. This approach is correct but incurs lock overhead on every access, which might be a performance concern.

WebSite/content/other/blog_00028.en.md#L121-260
// LazySingleton.h
#pragma once
#include <mutex>
#include <memory>

class LazySingleton {
private:
    static std::mutex mtx;
    static LazySingleton* instance;
    LazySingleton() = default;
public:
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

    static LazySingleton* GetInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (!instance) instance = new LazySingleton();
        return instance;
    }

    void Say();
};

// LazySingleton.cpp
std::mutex LazySingleton::mtx;
LazySingleton* LazySingleton::instance = nullptr;

This pattern may be appropriate if:

  • You can accept the extra lock cost, or
  • Your environment's performance characteristics make the lock cost negligible.

Meyers' Singleton uses a function-local static variable. Since C++11, initialization of function-local static variables is thread-safe, so this provides a concise, safe lazy-initialization approach.

WebSite/content/other/blog_00028.en.md#L261-360
// MeyersSingleton.h
#pragma once
#include <iostream>

class MeyersSingleton {
private:
    MeyersSingleton() { }
public:
    MeyersSingleton(const MeyersSingleton&) = delete;
    MeyersSingleton& operator=(const MeyersSingleton&) = delete;

    static MeyersSingleton& GetInstance() {
        static MeyersSingleton instance; // thread-safe since C++11
        return instance;
    }

    void Say() { std::cout << "MeyersSingleton\n"; }
};

Refer to cppreference for the guarantee that local static initialization is thread-safe: https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables

Lazy Singleton with std::call_once

std::call_once and std::once_flag provide an explicit and safe way to perform one-time initialization. This is useful for more complex initialization logic.

WebSite/content/other/blog_00028.en.md#L361-520
// CallOnceSingleton.h
#pragma once
#include <mutex>
#include <memory>

class CallOnceSingleton {
private:
    static std::unique_ptr<CallOnceSingleton> instance;
    static std::once_flag initFlag;
    CallOnceSingleton() = default;

    static void Init() {
        instance.reset(new CallOnceSingleton());
    }
public:
    CallOnceSingleton(const CallOnceSingleton&) = delete;
    CallOnceSingleton& operator=(const CallOnceSingleton&) = delete;

    static CallOnceSingleton& GetInstance() {
        std::call_once(initFlag, &CallOnceSingleton::Init);
        return *instance;
    }

    void Say();
};

// CallOnceSingleton.cpp
std::unique_ptr<CallOnceSingleton> CallOnceSingleton::instance;
std::once_flag CallOnceSingleton::initFlag;

Useful reference: https://en.cppreference.com/w/cpp/thread/call_once

Practical extensions and variations

Once you understand the basic pattern you can adapt it to various needs. The following are examples and not necessarily the best solution for every situation.

Template-based eager singleton: avoids repeating singleton boilerplate but may reduce readability as it hides the singleton nature in a template.

WebSite/content/other/blog_00028.en.md#L521-700
// TemplateEagerSingleton.h
#pragma once
#include <iostream>

template<typename T>
class TemplateEagerSingleton {
private:
    static T instance; // created at program startup
    TemplateEagerSingleton() = delete;
public:
    TemplateEagerSingleton(const TemplateEagerSingleton&) = delete;
    TemplateEagerSingleton& operator=(const TemplateEagerSingleton&) = delete;

    static T& GetInstance() {
        return instance;
    }
};

// static member definition (for templates, definition can be in header)
template<typename T>
T TemplateEagerSingleton<T>::instance;

// Foo.h
struct Foo {
    void Say() { std::cout << "TemplateEagerSingleton MyService\n"; }
};

// other.cpp
auto& foo = TemplateEagerSingleton<Foo>::GetInstance();
foo.Say();

Multi-instance eager approach: allows a fixed number of globally accessible instances. This changes the semantics from strict singleton to a small pool of global instances — lifecycle management and use-case justification should be considered.

WebSite/content/other/blog_00028.en.md#L701-920
// MultiInstanceEagerSingleton.h
#pragma once
#include <array>
#include <iostream>
#include <cstddef>
#include <cassert>

class MultiInstanceEagerSingleton {
private:
    static constexpr std::size_t COUNT = 3;
    static std::array<MultiInstanceEagerSingleton, COUNT> instances; // created at startup
    int id_;
    MultiInstanceEagerSingleton(int id = 0) : id_(id) {}
public:
    MultiInstanceEagerSingleton(const MultiInstanceEagerSingleton&) = delete;
    MultiInstanceEagerSingleton& operator=(const MultiInstanceEagerSingleton&) = delete;

    // Access one of several instances by index
    static MultiInstanceEagerSingleton& GetInstance(std::size_t index) {
        assert(index < COUNT && "index out of range");
        return instances[index];
    }

    void Say() { std::cout << "MultiInstanceEagerSingleton id=" << id_ << '\n'; }
};

// MultiInstanceEagerSingleton.cpp
#include "MultiInstanceEagerSingleton.h"

// static member definition (COUNT instances created at startup)
std::array<MultiInstanceEagerSingleton, MultiInstanceEagerSingleton::COUNT> MultiInstanceEagerSingleton::instances = {
    MultiInstanceEagerSingleton(0),
    MultiInstanceEagerSingleton(1),
    MultiInstanceEagerSingleton(2)
};

// other.cpp
auto& s0 = MultiInstanceEagerSingleton::GetInstance(0);
s0.Say();

When to use and alternatives

Before adopting a Singleton, consider:

  • Does the application truly need exactly one instance?
  • Do tests need to substitute the instance?
  • Is thread-safe initialization guaranteed for your chosen approach?

Alternatives:

  • Dependency Injection (DI): Pass dependencies into classes so tests can inject mocks/stubs.
  • Factory / Service Locator: Provides centralized management; Service Locator can hide global dependencies so use with caution.
  • Module-scoped singletons: In C++, managing a shared object at module/translation-unit scope may suffice.

Testability

Singletons introduce global state, which can make unit testing harder. Consider:

  • Providing test-only initialization/teardown hooks (careful to avoid leaking test-only APIs into production).
  • Allowing construction to be supplied externally so tests can inject mocks.
  • Meyers' Singleton (function-local static) lives until process exit and is difficult to reset; consider process-level isolation for tests or using DI for testability.

Anti-patterns and pitfalls

  • Overuse: Wrapping global variables in a Singleton just for convenience increases coupling.
  • Hidden global state: Dependencies become implicit, making testing and refactoring hard.
  • Lifecycle ambiguity: Static initialization order issues and unclear shutdown semantics when multiple static objects interact.

Summary

  • Singleton enforces a single instance and provides a global access point, but it comes with design trade-offs: testability, coupling, and lifecycle concerns.
  • In C++11 and later, the function-local static (Meyers' Singleton) provides a concise, thread-safe option.
  • Use std::call_once for explicit and robust one-time initialization when you need more control.
  • For large systems or when testability is a priority, prefer DI or factory-based approaches over singletons.

References

Mermaid diagrams

Class diagram (Singleton and consumer relationship):

WebSite/content/other/blog_00028.en.md#L921-980
%%{init:{'theme':'default'}}%%
classDiagram
    class MeyersSingleton {
        +static MeyersSingleton& GetInstance()
        +void Say()
    }
    class Consumer {
        +void useSingleton()
    }
    Consumer --> MeyersSingleton : uses

Sequence diagram (first call vs subsequent calls):

WebSite/content/other/blog_00028.en.md#L981-1040
sequenceDiagram
    participant Client
    participant Singleton
    Client->>Singleton: GetInstance() // 1st call
    alt not initialized
        Singleton-->>Singleton: initialize (constructor)
        Singleton-->>Client: return instance
    else already initialized
        Singleton-->>Client: return instance
    end

That’s it for this article. Next time we'll cover another GoF pattern with C++ examples and mermaid diagrams. The implementations shown here are simplified for learning; evaluate requirements (threads, lifecycle, testability) carefully before adopting in production.

— Pan-kun

← GoF Design Pa…← Back to BlogGoF Design Pa… →