Efficient Learning of Common C++ Design Patterns

Content from: Programmer Lao Liao

https://www.bilibili.com/video/BV1s5WVz9EwN/

Chapter 1: Singleton Pattern (C++)

The description of the Singleton pattern is: Ensure that a class has only one instance and provide a global access point to that instance.

The most important characteristic of the Singleton pattern is: A class can have at most one object. Intent: Ensure that there is a unique instance of a certain class in the system and provide a global access point.

Motivation: Global configurations, loggers, thread pools, connection pools, etc., only need one shared state and resource to avoid redundant creation, reduce competition, and minimize memory waste.

Class Diagram (Core Roles)

Efficient Learning of Common C++ Design Patterns

Sequence Diagram: First Access in Multithreading

Efficient Learning of Common C++ Design Patterns

Implementation Key Points

  • Thread safety and memory visibility are core interview topics

  • Since C++11, initialization of local static variables within functions is thread-safe

  • DCLP requires correct memory order (acquire/release) and pointer publishing

  • std::call_once ensures initialization is executed only once

Core Code (Comparison of Five Implementations)

Only key snippets are shown, complete runnable code can be found in src/.

// B站程序员老廖
// 1. Meyers Singleton (Recommended)
class MeyersSingleton {
public:
    static MeyersSingleton& instance() {
        static MeyersSingleton instance; // C++11 thread-safe
        return instance;
    }
    void doWork();
};
// 2. DCLP (Double-Checked Locking)
class DCLPSingleton {
public:
    /*
    // This is an incorrect implementation
    if (!instance_) {
        lock;
        if (!instance_) {
            instance_ = new X;
        }
    }
    */
    // The goal of Double-Checked Locking (DCLP)
    static DCLPSingleton* instance() {
        DCLPSingleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {  // Only enter the lock area if the instance has not been created
            std::lock_guard<std::mutex> lock(mutex_); // Lock and check again (to prevent multiple threads from entering creation)
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new DCLPSingleton();
                // "Release" this pointer, indicating "I am ready"
                instance_.store(tmp, std::memory_order_release); // Ensure: all previous operations (including construction) are completed
            }
        }
        return tmp; // First check: no lock, quickly return existing instance
    }
    // Problems with common locking scheme: every call requires locking, poor performance
    static DCLPSingleton* instance_common() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!instance_) {
            instance_ = new DCLPSingleton();
        }
        return instance_;
    }
private:
    static std::atomic<DCLPSingleton*> instance_;
    static std::mutex mutex_;
};
// 3. Call Once Singleton
class CallOnceSingleton {
public:
    static CallOnceSingleton& instance() {
        std::call_once(flag_, [](){ instance_.reset(new CallOnceSingleton()); });
        return *instance_;
    }
private:
    static std::once_flag flag_;
    static std::unique_ptr<CallOnceSingleton> instance_;
};
// 4. Eager Singleton (initialized immediately at program startup)
class EagerSingleton {
public:
    static EagerSingleton& instance() {
        return instance_; // Directly return, no need to check
    }
private:
    static EagerSingleton instance_; // Constructed at program startup
};
// 5. Lazy Singleton (initialized on first use, mutex ensures thread safety)
class LazySingleton {
public:
    static LazySingleton& instance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!instance_) {
            instance_.reset(new LazySingleton());
        }
        return *instance_;
    }
private:
    static std::unique_ptr<LazySingleton> instance_;
    static std::mutex mutex_;
};

Double-Checked Locking Pattern (DCLP)

The role and background of DCLP

1. Goal

To implement a thread-safe Singleton pattern, and after the first creation, subsequent calls to instance() should be lock-free and high-performance.

2. Problems with common locking schemes

static DCLPSingleton* instance() {
    std::lock_guard<std::mutex> lock(mutex_);
    if (!instance_) {
        instance_ = new DCLPSingleton();
    }
    return instance_;
}

✅ Correct ❌ Every call requires locking, poor performance

3. The goal of Double-Checked Locking (DCLP)

  • First check: no lock, quickly return existing instance

  • Only enter the lock area if the instance has not been created

  • Check again after locking (to prevent multiple threads from entering creation)

  • Store the instance after creation

This is the origin of “Double-Checked”.

Why do we need std::atomic and memory order?

This is the easiest part to misunderstand. Let’s look at the original code:

static DCLPSingleton* instance() {
    DCLPSingleton* tmp = instance_.load(std::memory_order_acquire);
    if (!tmp) {
        std::lock_guard<std::mutex> lock(mutex_);
        tmp = instance_.load(std::memory_order_relaxed);
        if (!tmp) {
            tmp = new DCLPSingleton();
            instance_.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

1. Why must instance_ be std::atomic<T*>?

Because multiple threads will concurrently read and write the instance_ pointer.

  • If atomic is not used, multiple threads reading and writing a non-atomic variable → data race → undefined behavior (UB)

  • std::atomic provides atomic guarantees, avoiding read/write tearing

✅ Therefore, atomic is necessary, not optional.

2. Why do we need memory_order_acquire and memory_order_release?

This is the most subtle part. Let’s break it down step by step.

❌ Problem: The compiler/CPU may reorder!

Consider this line of code:

tmp = new DCLPSingleton();
instance_.store(tmp, std::memory_order_release);

At a low level, new DCLPSingleton() involves two actions:

  1. Allocate memory

  2. Call the constructor to initialize the object

However, the compiler or CPU may reorder these operations, leading to:

The pointer instance_ is assigned first, but the object has not been constructed yet!

At this point, another thread executes:

DCLPSingleton* tmp = instance_.load(); // Sees a non-null pointer
if (!tmp) { ... } // Skips
return tmp; // Returns a "half-constructed" object! Crashes!

This is a typical safety issue caused by reordering.

Solution: Use acquire-release memory order to establish a “synchronization relationship”

We use:

  • load(std::memory_order_acquire): A get operation that ensures subsequent reads/writes cannot be reordered before it

  • store(std::memory_order_release): A release operation that ensures previous reads/writes cannot be reordered after it

When one thread executes store(release), and another thread executes load(acquire) and reads this value, it forms a synchronizes-with relationship.

This means: All write operations before release are visible to read operations after acquire.

For example:

Thread A (Creation):

tmp = new DCLPSingleton();           // Construction complete
instance_.store(tmp, release);       // Release pointer

Thread B (Reading):

tmp = instance_.load(acquire); // Reads non-null
// Due to acquire-release synchronization, guarantees visibility of the constructed object!

✅ Safe!

Why is the second check using relaxed?

tmp = instance_.load(std::memory_order_relaxed);

Because while holding the mutex lock, the mutex itself already provides synchronization semantics (mutex is acquire/release).

So this load is just to determine whether creation is needed, and does not require additional memory order constraints, using relaxed is more efficient.

High Frequency Interview Questions and In-Depth Analysis

Why is the Meyers Singleton thread-safe after C++11?

  • Static variable initialization within functions is provided by runtime with a “magic static” one-time, thread-safe mechanism, establishing happens-before between initialization and subsequent reads.

How to write DCLP (Double-Checked Locking Pattern) safely?

  • Use std::atomic<T*> pointer; on the publishing side store(memory_order_release), on the reading side load(memory_order_acquire); reading within the lock can use relaxed; avoid half-initialization and instruction reordering.

Trade-offs between call_once and Meyers?

  • Meyers is concise, lazy-loaded, zero boilerplate; call_once is convenient for custom destruction and resource management (like unique_ptr) and explicit initialization points.

How to handle static destruction order?

  • Destruction order across TUs is uncertain: can choose not to destruct (service processes), use std::atexit to register destruction, or provide shutdown() to specify release order.

Controlled destruction code snippet (across TU destruction order)

// B站程序员老廖
#include <cstdlib>
#include "singleton/singleton.hpp"
// At process exit, destruct singletons in order to avoid uncertain destruction order across TUs
static void destroy_singletons_in_order() {
  CallOnceSingleton::shutdown();  // First release controllable resources (unique_ptr)
  DCLPSingleton::destroy();       // Then release DCLP pointer instance
  // MeyersSingleton usually does not destruct actively (recovered by OS), avoiding destruction order risks
}
int main() {
  std::atexit(destroy_singletons_in_order);  // ... business logic
}

Running and Verification

See the build command in the root directory README. The executable will execute doWork() for each of the three implementations 9 times in a multithreaded environment and assert the count.

Counterexample: Non-thread-safe lazy singleton (for illustration)

class UnsafeLazySingleton {
public:
    static UnsafeLazySingleton* instance() {
        if (!instance_) instance_ = new UnsafeLazySingleton();
        return instance_;
    }
};

Multiple threads may create multiple instances through empty checks, and there is a publication-escape problem.

Practical Mapping

  • Loggers, configuration loading, thread pools, connection pools

  • Shared monitoring/metrics reporting across multiple modules (e.g., global MetricsRegistry)

Real Case: Thread-Safe Logger Singleton (C++)

Code location:

src/logger/logger.hpp, src/logger/examples_logger.cpp

Class Diagram

Efficient Learning of Common C++ Design Patterns

Sequence Diagram: Client Concurrently Writing Logs

Efficient Learning of Common C++ Design Patterns

Core Usage Code (Snippet)

Only key points are shown, complete examples can be found in src/logger/examples_logger.cpp

// B站程序员老廖
#include "logger/logger.hpp"
#include <thread>
#include <vector>
int main() {
  Logger::instance().logInfo("Starting service");
  std::vector<std::thread> ws;
  for (int i = 0; i < 4; ++i) {
    ws.emplace_back([i]{
      Logger::instance().logInfo("worker " + std::to_string(i) + " started");
      Logger::instance().logError("worker " + std::to_string(i) + " warning");
    });
  }
  for (auto& t : ws) t.join();
  Logger::instance().logInfo("Service exiting");
}

Interview Quick Answer Checklist

Why is the Meyers Singleton thread-safe?

  • C++11 magic static provides one-time, thread-safe initialization and establishes visibility.

What is the full name and Chinese of DCLP?

  • Double-Checked Locking Pattern, 双重检查锁定模式。

How is the visibility of DCLP guaranteed?

  • Atomic pointer + publishing side release, reading side acquire; reading within the lock can use relaxed.

How to avoid static destruction order issues?

  • Do not destruct or manage with atexit, or provide shutdown() to specify destruction order.

Chapter 2: Factory Method Pattern (C++)

The description of the Factory Method pattern is: Define an interface for creating objects, allowing subclasses to decide which class to instantiate. The Factory Method delays the instantiation of a class to its subclasses.

The most important characteristic of the Factory Method pattern is: Delaying object creation to subclasses, in line with the Open/Closed Principle.

Intent: Define an interface for creating objects, allowing subclasses to decide which class to instantiate. The Factory Method delays the instantiation of a class to its subclasses.

Motivation: When the system needs to create multiple related products, and the types of products may expand, directly using new in the client will lead to code coupling. The Factory Method allows specific factories to be responsible for creating specific products through an abstract factory interface, supporting modification-free expansion.

Class Diagram (Decomposed Display: Simple Factory vs Factory Method)

1. Structure of Simple Factory Pattern

Efficient Learning of Common C++ Design Patterns

2. Structure of Factory Method Pattern

Efficient Learning of Common C++ Design Patterns

3. Product Inheritance Relationship

Efficient Learning of Common C++ Design Patterns

Sequence Diagram: Factory Method Creating Products

Efficient Learning of Common C++ Design Patterns

Implementation Key Points

  • The Factory Method delays object creation to subclasses, with each specific factory responsible for creating the corresponding product

  • In line with the Open/Closed Principle: Adding a new product only requires adding a specific factory and specific product, without modifying existing code

  • Compared to Simple Factory: Simple Factory centralizes creation but violates the Open/Closed Principle, while Factory Method decentralizes creation but is easy to extend

  • Can be combined with Template Method Pattern to define common business processes in the abstract factory

Core Code (Simple Factory vs Factory Method vs Parameterized Factory)

Only key snippets are shown, complete runnable code can be found in src/factory/.

// B站程序员老廖
// Abstract Product
class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const std::string& message) = 0;
    virtual std::string getType() const = 0;
};
// Concrete Product
class FileLogger : public Logger {
public:
    explicit FileLogger(const std::string& filename) : filename_(filename) {}
    void log(const std::string& message) override {
        std::cout << "[FILE:" << filename_ << "] " << message << std::endl;
    }
    std::string getType() const override { return "FileLogger"; }
private:
    std::string filename_;
};
// 1. Simple Factory (violates Open/Closed Principle)
class SimpleLoggerFactory {
public:
    enum class LoggerType { FILE, CONSOLE, NETWORK };
    static std::unique_ptr<Logger> createLogger(LoggerType type, const std::string& param = "") {
        switch (type) {
            case LoggerType::FILE: return std::make_unique<FileLogger>(param);
            case LoggerType::CONSOLE: return std::make_unique<ConsoleLogger>();
            // Adding a new type requires modifying this part, violating the Open/Closed Principle
        }
    }
};
// 2. Factory Method (complies with Open/Closed Principle)
class LoggerFactory {
public:
    virtual ~LoggerFactory() = default;
    virtual std::unique_ptr<Logger> createLogger() = 0; // Delayed to subclasses
    // Template method: common business logic using factory method
    void processLog(const std::string& message) {
        auto logger = createLogger();
        logger->log("Processing: " + message);
    }
};
class FileLoggerFactory : public LoggerFactory {
public:
    explicit FileLoggerFactory(const std::string& filename) : filename_(filename) {}
    std::unique_ptr<Logger> createLogger() override {
        return std::make_unique<FileLogger>(filename_);
    }
private:
    std::string filename_;
};
// 3. Parameterized Factory (balancing convenience and extensibility)
class ParameterizedLoggerFactory {
public:
    using CreateFunc = std::function<std::unique_ptr<Logger>(const std::string&)>;
    static void registerLogger(const std::string& type, CreateFunc creator) {
        creators_[type] = creator; // Supports runtime registration of new types
    }
    static std::unique_ptr<Logger> createLogger(const std::string& type, const std::string& param = "") {
        auto it = creators_.find(type);
        return it != creators_.end() ? it->second(param) : nullptr;
    }
private:
    static std::unordered_map<std::string, CreateFunc> creators_;
};

High Frequency Interview Questions and In-Depth Analysis

What is the core difference between Simple Factory and Factory Method?

  • Simple Factory: Centralized creation logic, decides which product to create through parameters, but adding new products requires modifying the factory class (violates Open/Closed Principle). Factory Method: Delays creation to subclasses, each specific factory creates the corresponding product, adding new products only requires adding factory subclasses (complies with Open/Closed Principle).

Why is the Factory Method easier to extend?

  • Factory Method follows the principle of “open for extension, closed for modification”. When adding a new product type, only specific product classes and specific factory classes need to be added, without modifying existing code, reducing system coupling and the risk of introducing bugs.

What are the disadvantages of the Factory Method?

  • The number of classes increases exponentially (each product requires a corresponding factory class), increasing system complexity. For scenarios with fewer product types and infrequent changes, Simple Factory may be more suitable.

How to choose a factory pattern?

  • Fixed and few product types: Simple Factory. Many product types that need to be extended: Factory Method. Need runtime dynamic registration: Parameterized Factory (combined with registration pattern).

Running and Verification

See the build command in the root directory README. The executable will demonstrate the usage of three factory patterns, including verification of extensibility by registering new product types at runtime.

Real Case: Logging System Factory (C++)

Code location:

src/factory/factory.hpp, src/factory/examples.cpp

Business Scenario Class Diagram

Factory Class Inheritance Relationship

Efficient Learning of Common C++ Design Patterns

Product Class Inheritance Relationship

Efficient Learning of Common C++ Design Patterns

Core Usage Code (Snippet)

Only key points are shown, complete examples can be found in src/factory/examples.cpp

// B站程序员老廖
#include "factory/factory.hpp"
int main() {
    // 1. Simple Factory Usage
    auto logger1 = SimpleLoggerFactory::createLogger(
        SimpleLoggerFactory::LoggerType::FILE, "app.log");
    logger1->log("Log created by Simple Factory");
    // 2. Factory Method Usage
    std::unique_ptr<LoggerFactory> factory =
        std::make_unique<FileLoggerFactory>("method.log");
    factory->processLog("Message processed by Factory Method");
    // 3. Parameterized Factory Usage (supports runtime extension)
    auto logger2 = ParameterizedLoggerFactory::createLogger("file", "param.log");
    // Runtime registration of new type
    ParameterizedLoggerFactory::registerLogger("debug",
        [](const std::string& level) -> std::unique_ptr<Logger> {
            return std::make_unique<DebugLogger>(level);
        });
    auto debugLogger = ParameterizedLoggerFactory::createLogger("debug", "VERBOSE");
}

Interview Quick Answer Checklist

What is the essential difference between Simple Factory and Factory Method?

  • Simple Factory centralizes creation, while Factory Method decentralizes creation; the former violates the Open/Closed Principle, while the latter complies with it.

How does the Factory Method reflect the Open/Closed Principle?

  • When adding a new product, only specific product classes and specific factory classes need to be added, without modifying existing code.

What are the disadvantages of the Factory Method?

  • The number of classes increases exponentially, increasing system complexity; suitable for scenarios with many product types that need to be extended.

When to use Simple Factory?

  • When the product types are few and relatively fixed, and code simplicity is pursued; such as choosing the format of a configuration file parser.

There is too much content; if you need the content of the following chapters, you can watch the following video to obtain the complete learning document: https://www.bilibili.com/video/BV1s5WVz9EwN/

Chapter 3: Abstract Factory Pattern (C++)

Chapter 4: Strategy Pattern (C++)

Chapter 5: Observer Pattern (C++)

Chapter 6: Adapter Pattern (C++)

Chapter 7: Chain of Responsibility Pattern (C++)

Leave a Comment