Singleton in C++: Wie funktioniert es unter der Haube? Analyse der legendären Frage

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Einleitung: Warum Singleton nicht nur "ein Objekt" ist



Wenn Sie jemals auf Stack Overflow nach einer Antwort auf die Frage gesucht haben: "How do you implement the Singleton design pattern?", dann haben Sie Tausende von Stimmen, Dutzende von Varianten und hitzige Diskussionen in den Kommentaren gesehen. Diese Frage ist zum Klassiker geworden, weil Singleton eines der am einfachsten zu verstehenden, aber für eine korrekte Implementierung tückischsten Patterns ist.



In diesem Artikel zeigen wir nicht nur, wie man Singleton in C++ schreibt. Wir schauen unter die Haube: Wir analysieren, wie der statische Speicher funktioniert, was Initialisierungsgarantien sind, warum ein naiver Ansatz die Leistung beeinträchtigen kann und wie die endgültige Version (Meyers Singleton) zum De-facto-Standard wurde.



Wenn Sie sich auf ein Vorstellungsgespräch vorbereiten oder C++ tiefer verstehen möchten, ist dieser Artikel genau das Richtige. Los geht's!



Was ist Singleton? Eine kurze Erinnerung



Singleton (Einzelstück) ist ein Erzeugungsmuster, das sicherstellt, dass eine Klasse nur eine Instanz hat, und einen globalen Zugriffspunkt darauf bereitstellt.



Was könnte da schon schiefgehen? In C++ gibt es jedoch Nuancen:

  • Threadsicherheit bei der ersten Erstellung des Objekts.
  • Lebenszyklusverwaltung (Wann wird das Objekt zerstört?).
  • Verhindern von Kopieren und Zuweisen.
  • Probleme mit Multithreading (Data Race).


Betrachten wir die Entwicklung der Implementierung – von der naiven bis zur produktionsreifen.



Naive Implementierung: Was ist falsch?



Der einfachste Weg ist, den Konstruktor privat zu machen und einen statischen Zeiger zu speichern:



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

void doSomething() { // Logik }

private: Singleton() {} // privater Konstruktor Singleton(const Singleton&) = delete; // Kopieren verboten Singleton& operator=(const Singleton&) = delete; // Zuweisen verboten

static Singleton* instance;};

// Definition des statischen MembersSingleton* Singleton::instance = nullptr;


Probleme:

  1. Nicht threadsicher. Wenn zwei Threads gleichzeitig getInstance() aufrufen, sehen beide nullptr und erstellen zwei Objekte. Dies verletzt die Singleton-Invariante.
  2. Speicherleck. Das mit new erstellte Objekt wird nicht automatisch gelöscht. (Obwohl der Speicher bei Prozessende an das OS zurückgegeben wird, wird der Destruktor nicht aufgerufen).
  3. Problem der statischen Initialisierung. Wenn ein anderes statisches Objekt versucht, Singleton vor seiner Initialisierung zu verwenden – UB.


Man könnte einen Mutex hinzufügen, aber das verlangsamt jeden Aufruf von getInstance(), selbst wenn das Objekt bereits erstellt wurde. Schlecht.



Double-Checked Locking (DCLP): Fast richtig, aber gefährlich



Eine beliebte Optimierung ist, den Zeiger zweimal zu prüfen: zuerst ohne Sperre, dann mit Sperre. Dies wird als Double-Checked Locking Pattern bezeichnet.



#include <mutex>

class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { // Erste Prüfung (ohne Sperre) std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // Zweite Prüfung (mit Sperre) instance = new Singleton(); } } return instance; }

private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;

static Singleton* instance; static std::mutex mutex;};

Singleton* Singleton::instance = nullptr;std::mutex Singleton::mutex;<

Blogs

Buchempfehlungen