Fundamentals 26 min read

Mastering C++ Singleton: 5 Implementations, Pitfalls & Performance Tips

This article explains why the singleton pattern is essential for global resources in C++, walks through five concrete implementations—from basic lazy and eager versions to thread‑safe double‑checked locking and C++11 static locals—analyzes their performance and resource trade‑offs, and provides a complete printer‑singleton case study with build and run instructions.

Deepin Linux
Deepin Linux
Deepin Linux
Mastering C++ Singleton: 5 Implementations, Pitfalls & Performance Tips

Part 1: Introduction to the Singleton Pattern

For C++ developers, many scenarios such as logging, configuration management, or database connection pools require a globally unique instance. The singleton pattern guarantees that a class has exactly one instance throughout the program’s lifetime and provides a global access point.

Analogies like a company’s sole CEO or a school’s single principal illustrate the concept: all modules request the unique instance for coordinated decisions.

Benefits include memory savings, consistent global resource management, and avoidance of duplicate resource allocation, especially in multithreaded environments.

Part 2: Hand‑written C++ Singleton Implementations

2.1 Core Requirements

Make the constructor private, hold a private static pointer to the sole instance, and expose a public static accessor.

class Singleton {
private:
    Singleton() {}          // private constructor
    static Singleton* instance;
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

2.2 Lazy (Non‑Thread‑Safe) Version

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
private:
    static Singleton* instance;
    Singleton() {}
};
Singleton* Singleton::instance = nullptr;

This delays creation until first use but fails under concurrent access.

2.3 Lazy (Thread‑Safe with Mutex)

#include <mutex>
class Singleton {
public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mutex);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
private:
    static Singleton* instance;
    static std::mutex mutex;
    Singleton() {}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

Each call acquires a lock, guaranteeing safety but adding overhead.

2.4 Double‑Checked Locking (DCL)

#include <mutex>
class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
private:
    static Singleton* instance;
    static std::mutex mutex;
    Singleton() {}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

First checks avoid locking after the instance is created; the second check prevents race conditions.

2.5 Eager (Hungry) Version

class Singleton {
public:
    static Singleton* getInstance() { return instance; }
private:
    static Singleton* instance;
    Singleton() {}
};
Singleton* Singleton::instance = new Singleton();

The instance is created at program start, making it inherently thread‑safe but possibly wasteful.

2.6 C++11 Local‑Static Implementation

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance; // thread‑safe initialization
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

Uses a function‑local static variable; C++11 guarantees safe initialization with minimal code.

Part 3: Comparison of Implementations

3.1 Performance

Eager initialization offers O(1) access with no checks. Lazy versions incur a check; the mutex‑protected lazy version also incurs lock/unlock overhead on every call. DCL reduces locking to the first creation, and the C++11 local‑static version provides O(1) access after the one‑time thread‑safe initialization.

3.2 Resource Usage

All approaches allocate a single instance (O(1) memory). Mutex‑based implementations add a small amount of extra memory for the lock object.

3.3 Recommended Scenarios

Single‑threaded programs: non‑thread‑safe lazy version.

Multithreaded programs with tiny resources: eager version.

Large resources with need for lazy loading: DCL or C++11 local‑static version (preferred for modern C++).

Part 4: Practical Case Study – Printer Singleton

4.1 Definition

A class that ensures only one printer object exists, creates it internally, and provides a global access method.

The class disallows copying and assignment.

It offers methods to print documents, query queue length, and destroy the instance.

4.2 Code Files

singleton.h

#ifndef SINGLETON_H
#define SINGLETON_H

#include <mutex>
#include <string>

class Printer {
public:
    Printer(const Printer&) = delete;
    Printer& operator=(const Printer&) = delete;
    static Printer* getInstance();
    void printDocument(const std::string& documentName);
    int getPrintQueueLength() const;
    static void destroyInstance();
protected:
    Printer();
    ~Printer();
private:
    static Printer* instance;
    static std::mutex mtx;
    int printQueueLength;
};

#endif // SINGLETON_H

singleton.cpp

#include "singleton.h"
#include <iostream>
#include <chrono>
#include <thread>

Printer* Printer::instance = nullptr;
std::mutex Printer::mtx;

Printer::Printer() : printQueueLength(0) {
    std::cout << "Printer instance initialized" << std::endl;
}

Printer::~Printer() {
    std::cout << "Printer instance destroyed" << std::endl;
}

Printer* Printer::getInstance() {
    if (instance == nullptr) {
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {
            instance = new Printer();
        }
    }
    return instance;
}

void Printer::printDocument(const std::string& documentName) {
    std::lock_guard<std::mutex> lock(mtx);
    ++printQueueLength;
    std::cout << "
Start printing: " << documentName << std::endl;
    std::cout << "Queue length: " << printQueueLength << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Document " << documentName << " printed" << std::endl;
    --printQueueLength;
    std::cout << "Queue length: " << printQueueLength << std::endl;
}

int Printer::getPrintQueueLength() const { return printQueueLength; }

void Printer::destroyInstance() {
    std::lock_guard<std::mutex> lock(mtx);
    if (instance) {
        delete instance;
        instance = nullptr;
    }
}

main.cpp

#include "singleton.h"
#include <iostream>
#include <thread>
#include <vector>
#include <string>

void printTask(const std::string& doc) {
    Printer* printer = Printer::getInstance();
    printer->printDocument(doc);
}

int main() {
    std::cout << "=== Printer Singleton Test ===" << std::endl;

    // Single‑thread test
    std::cout << "
--- Single‑thread test ---" << std::endl;
    Printer* printer = Printer::getInstance();
    printer->printDocument("Report.pdf");
    printer->printDocument("Image.png");

    // Multi‑thread test
    std::cout << "
--- Multi‑thread test ---" << std::endl;
    std::vector<std::thread> threads;
    for (int i = 1; i <= 5; ++i) {
        threads.emplace_back(printTask, "Doc" + std::to_string(i) + ".txt");
    }
    for (auto& t : threads) t.join();

    // Verify uniqueness
    std::cout << "
--- Uniqueness verification ---" << std::endl;
    Printer* printer2 = Printer::getInstance();
    if (printer == printer2) {
        std::cout << "Success: both pointers refer to the same instance" << std::endl;
    }

    Printer::destroyInstance();
    return 0;
}

A simple Makefile compiles the three files with g++ -std=c++11 -pthread. Running the program shows initialization, sequential printing messages (protected by the mutex), verification of the singleton property, and final destruction.

4.3 Observed Output

=== Printer Singleton Test ===

--- Single‑thread test ---
Printer instance initialized
Start printing: Report.pdf
Queue length: 1
... (2‑second delay) ...
Document Report.pdf printed
Queue length: 0
Start printing: Image.png
Queue length: 1
... (2‑second delay) ...
Document Image.png printed
Queue length: 0

--- Multi‑thread test ---
Start printing: Doc1.txt
Queue length: 1
... (each thread prints in turn, queue length stays 1) ...

--- Uniqueness verification ---
Success: both pointers refer to the same instance
Printer instance destroyed
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsCthread safetyC++11singleton pattern
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.