**Implementing a Thread‑Safe Singleton in Modern C++**

In many codebases you need a single, shared instance of a class – a classic Singleton.
With C++11 and beyond, the language gives us built‑in guarantees that make the implementation straightforward and safe across threads.
Below is a minimal, self‑contained example that also showcases some C++17 features for clarity.

#include <iostream>
#include <mutex>
#include <memory>
#include <thread>
#include <vector>

// ------------------------------------------------------------
// 1. Singleton class
// ------------------------------------------------------------
class Logger {
public:
    // Deleted copy/move constructors and assignment operators
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    Logger(Logger&&) = delete;
    Logger& operator=(Logger&&) = delete;

    // Accessor for the singleton instance
    static Logger& instance() {
        // Guaranteed to be thread‑safe since C++11
        static Logger instance;
        return instance;
    }

    // Public API
    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[" << std::this_thread::get_id() << "] " << message << '\n';
    }

private:
    Logger() = default;        // Private constructor
    ~Logger() = default;

    std::mutex mutex_;         // Protects std::cout
};

// ------------------------------------------------------------
// 2. Test code that spawns multiple threads
// ------------------------------------------------------------
void worker(int id) {
    for (int i = 0; i < 3; ++i) {
        Logger::instance().log("Thread " + std::to_string(id) + " message " + std::to_string(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    const int threadCount = 5;
    std::vector<std::thread> threads;
    threads.reserve(threadCount);

    for (int i = 0; i < threadCount; ++i) {
        threads.emplace_back(worker, i);
    }

    for (auto& t : threads) t.join();
    return 0;
}

Why does this work?

Feature Explanation
Local static variable (static Logger instance;) The C++11 standard guarantees that initialization of a local static variable is thread‑safe. Only one thread will perform the construction, others will wait until the object is fully constructed.
Deleted copy/move semantics Prevents accidental copying or moving of the Singleton, preserving the unique instance guarantee.
std::mutex inside the class Serialises access to std::cout. Without it, concurrent writes could interleave and corrupt the output.

Optional Enhancements

  • Lazy initialization with std::call_once – not necessary because of the local static guarantee, but still a valid approach if you prefer explicit control.
  • std::shared_ptr for the instance – useful if you want the Singleton to be destructible at program exit or to allow dynamic replacement.
  • RAII wrapper for logging – e.g., an auto log = Logger::instance().write("msg"); that automatically flushes on scope exit.

Common Pitfalls

  1. Static local variables in functions defined in headers – If the function is defined in a header included by multiple translation units, each TU gets its own static. Move the function into a source file or use an inline function with inline specifier (C++17) to ensure a single instance.
  2. Order of destruction – If the Singleton holds resources that other static objects depend on during destruction, you might need to manage destruction order explicitly or use the Meyers Singleton pattern shown above (it guarantees destruction after main returns).

By leveraging the language’s guarantees and keeping the implementation simple, you get a robust, thread‑safe Singleton that is both idiomatic C++ and easy to maintain.

发表评论