Singleton Design Pattern in C++
In diesem Artikel geht es um das Singleton Design Pattern in C++ und darum, warum es ein schlechtes Design Pattern ist. In meinem Source Code möchte ich euch nicht nur zeigen wie man ein Singleton erzeugt sondern auch wo die Gefahren davon liegen und worauf man bei größeren Projekten unbedingt achten sollte, beziehungsweise welche Alternativen man hat.
Singleton Design Pattern in C++
Es ist sehr einfach eine Klasse als Singleton Design Pattern in C++ umzusetzen. Eine solche Klasse hat dann folgende Eigenschaften:
- Eindeutigkeit
ein Singleton existiert maximal einmal im Speicher. Einmal instanziert holt man beim Aufruf immer die selbe Instanz. - Lazy Instanzierung
ein Singleton unterliegt einer „lazy initialization“. Es verwendet erst dann Speicher, wenn es zur Laufzeit erzeugt wurde und nicht wie normale statische Klassen bereits zur Initialisierung des Programms.
Sehen wir uns aber zuerst einmal die Verwendung im Code anhand eines Beispiels, eines Loggers an.
Beispiel
Für unser Rollenspiel, dass wir bereits beim Factory Design Pattern und beim Strategy Design Pattern angefangen haben benötigen wir nun in weiterer Folge ein Log. Dazu erzeugen wir ein Singleton, damit es von überall verwendet werden kann. Dieses Singleton wird dabei gleich hinsichtlich Kritik an dem Pattern entwickelt.
Implementierung
Der Source Code ist auf GitHub erreichbar. Um den Source Code mit g++ zu compilieren ist das Argument -std=c++11 erforderlich. Aus diesem Standard wird Code verwendet, um das Singleton vor parallelem Zugriff unterschiedliche Prozesse zu schützen.
Logger.h
Der Relevante Teil für das Singleton im Header:
class Logger { public: // Returns a reference to the singleton Logger object static Logger& instance(); ... protected: // Static variable for the one-and-only instance static Logger* pInstance; ...
Der Logger besitzt eine statische Variable mit der Referenz auf sich selbst. D.h. die Klasse besitzt, sobald sie instanziert ist einen Pointer auf den eigenen Speicherbereich. Die instance() Methode liefert diesen zurück.
Logger.cpp
Ausprogrammiert sieht das wie folgt aus:
Logger& Logger::instance() { static Cleanup cleanup; lock_guard guard(sMutex); if (pInstance == nullptr) pInstance = new Logger(); return *pInstance; }
Die Instance variable wird mit einem Nullpointer initialisiert. Sofern das der Fall ist, wird ein neues Logger Objekt erstellt und man merkt sich dessen Pointer. Jedesmal wenn diese Methode nun bei angelegter Instanz aufgerufen wird, dann bekommt man immer nur den selben Pointer zurück. Es ist nicht möglich das Objekt ein zweites Mal im Speicher anzulegen.
main.cpp
Im Programm kann jederzeit der Logger über Logger::instance() verwendet werden. Das sieht dann beispielsweise so aus:
int main(int argc, char **argv) { Logger::instance().log("Player entered realm", Logger::Debug); vector items = {"Dragon not found", "Orc not found"}; Logger::instance().log(items, Logger::Error); return 0; }
Locking
In dem Beispielcode verwende ich lock_guard, eine Möglichkeit die seit C++ 11 existiert und mit der man Ressourcen vor gleichzeitigen Zugriff schützt. Lock Guard funktioniert wie folgt:
When a lock_guard object is created, it attempts to take ownership of the mutex it is given. When control leaves the scope in which the lock_guard object was created, the lock_guard is destructed and the mutex is released.
Das Problem mit Singletons und Locks ist ein viel besprochenes Problem, welches beispielsweise in diesem Paper erläutert wird.
Kritik
Bei der Implementierung einer Singleton Klasse gibt es einige wichtige Informationen zu beachten. Besonders kritisch zu sehen ist dabei die Verwendung auf Multicore Umgebungen. Sobald mehrere Prozess gleichzeitig Daten in die Singleton Klasse speichern können wird es zur Laufzeit zufällig eintretend zu Überschneidungen kommen. Das resultiert dann in Effekten wie Abstürzen oder falschen Daten. Aus diesem Grund soll man nicht verleitet sein, das Singleton als Ablage globaler Variablen zu missbrauchen. Ein Beispiel?
Im Singleton gibt es eine Variable mobs, in der die Anzahl an gegnerischen Monstern im Dungeon gespeichert wird. Jedesmal wenn ein Gegner spawned wird dieses um eines erhöht, wird ein Gegner besiegt, dann um eins reduziert. Wegen der Parallelität heutiger Multicore Prozessoren ist es möglich, dass die Singleton Ressource von beiden Prozessen verwendet wird. Ohne speziellen Schutz könnte es so dazu kommen, dass der eine Prozess den bestehenden Wert erhöht und der andere diesen um den Wert vermindert. In diesem Fall wäre die Gesamtzahl danach geringer als tatsächlich.
Nähere Informationen dazu findet man in einem sehr interessanten Artikel zu dem Thema.
Fazit
Das Singleton Design Pattern ist vermutlich das am öftesten umgesetzte und gleichermaßen falsch verwendete Pattern überhaupt. Gerade auf aktuellen Systemen mit vielen Threads kann man da viel falsch machen und sollte Alternativen finden. Die klassische Anwendung als Log habe ich selber in C++ implementiert und dabei gleich für die Anwendung threadsicher gemacht.
You should fix „&“ to look like „&“