Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing the Singleton Design Pattern in C++

Tech 2

The Singleton pattern is a creational design pattern intended to ensure that a class has exactly one instance while providing a global point of access to that instance. This pattern is particularly valuable when coordinating actions across a system or managing shared resources where multiple instances would lead to inconsistent state or excessive resource consumption.

Fundamental Concepts of Singletons

The pattern is defined by three primary characteristics:

  1. Controlled Instantiation: The class prevents external entities from creating new instances by making its constructer private. It manages its own lifecycle.
  2. Unique Instance Storage: A private static member within the class holds the reference to the single existing object.
  3. Static Access Method: A public static function (often named getInstance) provides the mechanism for users to retrieve the instance. If the instance does not exist, the class creates it; otherwise, it returns the existing one.

Common Use Cases

  • Configuration Managers: Centralizing application settings to ensure consistency.
  • Hardware Interface Access: Managing access to a specific piece of hardware (like a serial port or printer) where concurrent access must be serialized.
  • Connection Pooling: Managing a cache of database connections to improve performance and limit resource usage.
  • Audit Logging: Directing all log messages through a single service to maintain chronological order and centralize output handling.

Implementation Variants in C++

Lazy Initialization (Non-Thread-Safe)

In a basic lazy implementation, the instance is created only when it is requested for the first time. This saves resources if the object is never used.

#include <iostream>

class BasicManager {
public:
    static BasicManager* get_instance() {
        if (m_instance == nullptr) {
            m_instance = new BasicManager();
        }
        return m_instance;
    }

private:
    BasicManager() = default;
    ~BasicManager() = default;

    // Prohibit copying and assignment
    BasicManager(const BasicManager&) = delete;
    BasicManager& operator=(const BasicManager&) = delete;

    static BasicManager* m_instance;
};

BasicManager* BasicManager::m_instance = nullptr;

Note that this version is not safe for multi-threaded environments, as two threads might simultaneously check if m_instance is null and create two separate objects.

Eager Initialization

Eager initialization creates the instance as soon as the program starts (during static initialization). This approach is inherently thread-safe in most scenarios because the instance is created before the main function executes.

class EagerProvider {
public:
    static EagerProvider& fetch() {
        return m_handle;
    }

private:
    EagerProvider() = default;
    EagerProvider(const EagerProvider&) = delete;
    EagerProvider& operator=(const EagerProvider&) = delete;

    static EagerProvider m_handle;
};

EagerProvider EagerProvider::m_handle;

Thread-Safe Lazy Initialization (Double-Checked Locking)

To ensure thread safety while maintaining lazy initialization, double-checked locking can be used alongside a mutex. This reduces overhead by only locking during the first creation phase.

#include <mutex>
#include <memory>

class SecureRegistry {
public:
    static SecureRegistry& get_access() {
        if (m_ptr == nullptr) {
            std::lock_guard<std::mutex> lock(m_sync_obj);
            if (m_ptr == nullptr) {
                m_ptr.reset(new SecureRegistry());
            }
        }
        return *m_ptr;
    }

private:
    SecureRegistry() = default;
    SecureRegistry(const SecureRegistry&) = delete;
    SecureRegistry& operator=(const SecureRegistry&) = delete;

    static std::unique_ptr<SecureRegistry> m_ptr;
    static std::mutex m_sync_obj;
};

std::unique_ptr<SecureRegistry> SecureRegistry::m_ptr = nullptr;
std::mutex SecureRegistry::m_sync_obj;

Modern C++ Thread-Safe Singleton (Meyers Singleton)

Since C++11, the initialization of static local variables is guaranteed to be thread-safe. This is the cleanest and most efficient way to implement a singleton in modern C++.

class ModernSingleton {
public:
    static ModernSingleton& instance() {
        static ModernSingleton obj;
        return obj;
    }

private:
    ModernSingleton() = default;
    ModernSingleton(const ModernSingleton&) = delete;
    ModernSingleton& operator=(const ModernSingleton&) = delete;
};

Practical Example: Global Audit Logger

This example demonstrates a thread-safe logging utility that writes system events to a local file using std::call_once for initialization.

#include <iostream>
#include <fstream>
#include <string>
#include <mutex>
#include <memory>

class SystemAuditLogger {
public:
    static SystemAuditLogger& get_logger() {
        std::call_once(m_init_flag, []() {
            m_instance.reset(new SystemAuditLogger());
        });
        return *m_instance;
    }

    void record_event(const std::string& log_entry) {
        std::lock_guard<std::mutex> lock(m_file_mutex);
        std::ofstream file("audit_log.txt", std::ios::app);
        if (file.is_open()) {
            file << "[LOG]: " << log_entry << std::endl;
        }
    }

private:
    SystemAuditLogger() = default;
    SystemAuditLogger(const SystemAuditLogger&) = delete;
    SystemAuditLogger& operator=(const SystemAuditLogger&) = delete;

    static std::unique_ptr<SystemAuditLogger> m_instance;
    static std::once_flag m_init_flag;
    std::mutex m_file_mutex;
};

std::unique_ptr<SystemAuditLogger> SystemAuditLogger::m_instance = nullptr;
std::once_flag SystemAuditLogger::m_init_flag;

// Usage function
void emit_log(const std::string& text) {
    SystemAuditLogger::get_logger().record_event(text);
}

Evaluation of the Singleton Pattern

Advantages

  • Resource Efficiency: Prevents unnecessary instantiation of heavy objects.
  • Namespace Cleanliness: Avoids polluting the global namespace with global variables while offering similar accessibility.
  • Controlled Access: Provides a strict mechanism for interacting with a sensitive resource.

Disadvantages

  • Tight Coupling: Code that depends on a singleton becomes harder to test because the dependency is hidden and difficult to mock.
  • State Persistence: Singletons maintain state for the entire duration of the application, wich can lead to side effects in unit tests.
  • Violation of Single Responsibility: The class is responsible both for its core business logic and for managing its own lifecycle.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.