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.
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_Hsingleton.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 destroyedSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
