Skip to Content
Course content

135: The Singleton Pattern in C++

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing code from developers moving into mid-level roles: they think a Singleton is just a class where every member is marked static. They essentially create a "namespace with a fancy name" and call it a pattern. But there is a massive difference between a collection of static functions and a Singleton.

Static Classes Aren't Singletons

If you just make your members static, you haven't actually controlled the lifecycle of an object; you've just created global state. You lose the ability to use inheritance, you can't implement interfaces, and you have zero control over when that state is initialized. Worse, if you try to implement a "naive" Singleton using a static pointer, you usually end up with a thread-safety nightmare.

// The "Naive" (and broken) way
class ConfigManager {
private:
    static ConfigManager* instance;
    ConfigManager() {} // Private constructor

public:
    static ConfigManager* getInstance() {
        if (instance == nullptr) { 
            // RACE CONDITION: Two threads could enter here simultaneously
            instance = new ConfigManager();
        }
        return instance;
    }
};

In the code above, if two threads call getInstance() at the exact same millisecond, you might end up with two different ConfigManager objects in memory. Now your "Single" ton is a doubleton, and your app is crashing in ways that are nearly impossible to debug.

The Meyers Singleton: The Gold Standard

You don't need complex mutexes or double-checked locking anymore. Since C++11, the language guarantees that static local variables are initialized in a thread-safe manner. This is known as the Meyers Singleton. It's cleaner, faster, and handles the cleanup for you.

Let's look at a real-world scenario: a LogService. You want one single point of truth for your logs across the entire application, but you don't want to pass a logger pointer into every single function in your codebase.

#include <iostream>
#include <string>
#include <mutex>

class LogService {
public:
    // 1. Delete copy constructor and assignment operator
    // We absolutely cannot allow copies of a Singleton.
    LogService(const LogService&) = delete;
    LogService& operator=(const LogService&) = delete;

    static LogService& getInstance() {
        // 2. Local static variable. 
        // C++11 guarantees this is initialized only once and is thread-safe.
        static LogService instance; 
        return instance;
    }

    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(logMutex);
        std::cout < "[LOG]: " < message < std::endl;
    }

private:
    // 3. Private constructor prevents direct instantiation
    LogService() {
        std::cout < "LogService Initialized." < std::endl;
    }

    std::mutex logMutex;
};

Notice a few things here. First, I deleted the copy constructor and assignment operator. If you don't do this, someone could accidentally write LogService myLog = LogService::getInstance();, which creates a copy and completely defeats the purpose of the pattern. Second, we return a reference (LogService&) rather than a pointer. This tells the user that the instance is guaranteed to exist; they don't need to check for nullptr.

When to Actually Use This (And When to Run Away)

I'll be honest with you: the Singleton is often called an "anti-pattern." Why? Because it's essentially a global variable in a tuxedo. It makes unit testing a pain because you can't easily swap the Singleton for a "mock" object during tests.

Use a Singleton when you have a truly unique resource—like a hardware driver interface, a filesystem cache, or a global configuration coordinator—where having two instances would actually be a logic error or a resource conflict. If you're just using it because you're too lazy to pass a pointer through a few function calls, you're creating technical debt. Use it sparingly, and use it intentionally.




📋 Practical Task

Exercise: Implement a Thread-Safe GameAudioManager

You are building a game engine. The audio hardware can only be initialized once. If multiple instances of an audio manager try to seize the hardware, the game will crash.

Your Task: Create a class named GameAudioManager that implements the Singleton pattern using the Meyers approach. Your implementation must meet these requirements:

  • The constructor must be private.
  • The copy constructor and copy assignment operator must be deleted.
  • It must provide a static method getInstance() that returns a reference to the single instance.
  • It must have a method playSound(std::string soundName) that prints "Playing sound: [soundName]" to the console.
  • Verify in your main() function that calling getInstance() multiple times returns the exact same memory address.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.