Patterns in der Softwareentwicklung: Das Singleton-Muster
Das Singleton Pattern gilt als das umstrittenste Entwurfmuster aus dem klassischen Buch "Design Patterns".
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/1/0/0/8/9/shutterstock_1185914659-3a9ae8c8bc4c2728.jpeg)
(Bild: Blackboard/Shutterstock.com)
- Rainer Grimm
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das umstrittenste Entwurfsmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software" (Design Pattern) ist das Singleton Pattern. Ich möchte es zunächst vorstellen, bevor ich seine Vor- und Nachteile diskutiere.
Das Singleton Muster ist ein Erzeugungsmuster. Hier sind die Fakten kurz und bündig:
Singleton Muster
Zweck
- Sichert zu, dass nur eine Instanz einer Klasse existiert
Anwendungsfall
- Man braucht Zugang zu einer gemeinsamen Ressource und
- diese gemeinsame Ressource darf nur einmal vorhanden sein.
Beispiel
- Die Boost-Serialisierung definiert ein Template, "which will convert any class into a singleton with the following features".
- Das Template erfüllt seine Aufgabe, indem es von der Klasse
boost::noncopyable
ableitet:
class BOOST_SYMBOL_VISIBLE singleton_module :
public boost::noncopyable
{
...
};
Struktur
instance (static)
- Eine private Instanz von Singleton
getInstance (static)
- Eine öffentliche Mitgliedsfunktion, die eine Instanz zurückgibt
- Erzeugt die Instanz
Singleton
- Privater Konstruktor
Der Aufruf der Mitgliedsfunktion getInstance
ist die einzige Möglichkeit, ein Singleton
zu erstellen. Außerdem darf das Singleton
keine Kopiersemantik unterstützen.
Das bringt mich zu seiner Implementierung in modernem C++.
Implementierung
In den folgenden Zeilen diskutiere ich verschiedene Implementierungsvarianten des Singleton. Beginnen möchte ich mit der klassischen Implementierung des Singleton-Musters.
Klassische Implementierung
Folgende Implementierung basiert auf dem Buch "Design Patterns":
// singleton.cpp
#include <iostream>
class MySingleton{
private:
static MySingleton* instance; // (1)
MySingleton() = default;
~MySingleton() = default;
public:
MySingleton(const MySingleton&) = delete;
MySingleton& operator=(const MySingleton&) = delete;
static MySingleton* getInstance(){
if ( !instance ){
instance = new MySingleton();
}
return instance;
}
};
MySingleton* MySingleton::instance = nullptr; // (2)
int main(){
std::cout << '\n';
std::cout << "MySingleton::getInstance(): "
<< MySingleton::getInstance() << '\n';
std::cout << "MySingleton::getInstance(): "
<< MySingleton::getInstance() << '\n';
std::cout << '\n';
}
Die ursprüngliche Implementierung verwendete einen protected
Default-Konstruktor. Zudem habe ich die Kopiersemantik (Kopierkonstruktor und Kopierzuweisungsoperator) explizit gelöscht. Ich werde später mehr über die Kopiersemantik und die Move-Semantik schreiben. Die Ausgabe des Programms zeigt, dass es nur eine Instanz der Klasse MySingleton
gibt.
Diese Implementierung des Singleton setzt den C++11 Standard voraus.
Mit C++17 kann die Deklaration (1) und Definition (2) der statischen Instanzvariable instance
direkt in der Klasse erfolgen:
class MySingleton{
private:
inline static MySingleton* instance{nullptr}; // (3)
MySingleton() = default;
~MySingleton() = default;
public:
MySingleton(const MySingleton&) = delete;
MySingleton& operator=(const MySingleton&) = delete;
static MySingleton* getInstance(){
if ( !instance ){
instance = new MySingleton();
}
return instance;
}
};
(3) führt die Deklaration und Definition in einem Schritt durch. Wie sieht es mit den Schwächen dieser Implementierung aus? Das Static Initialization Order Fiasko und Concurrency drängen sich deutlich auf.
Static Initialization Order Fiasco
Statische Variablen in einer Übersetzungseinheit werden entsprechend der Reihenfolge ihrer Definition initialisiert. Bei der Initialisierung statischer Variablen zwischen Übersetzungseinheiten gibt es dagegen ein schwerwiegendes Problem. Wenn eine statische Variable staticA
in einer Übersetzungseinheit und eine andere statische Variable staticB
in einer anderen Übersetzungseinheit definiert ist und staticB staticA
benötigt, um sich selbst zu initialisieren, kommt es gerne zum Static Initialization Order Fiasco: Das Programm ist fehlerhaft, weil es keine Garantie gibt, welche statische Variable zur Laufzeit zuerst initialisiert wird.
Der Vollständigkeit halber: Statische Variablen werden in der Reihenfolge ihrer Definition initialisiert und in der umgekehrten Reihenfolge wieder zerstört. Dementsprechend gibt es keine Garantie für die Reihenfolge der Initialisierung oder Zerstörung zwischen Übersetzungseinheiten. Eine Übersetzungseinheit ist das Ergebnis der Ausführung des Präprozessors.
Wie hängt das mit Singletons zusammen? Singletons sind verkleidete statische Variablen. Wenn also die Initialisierung eines Singleton von der Initialisierung eines weiteren Singleton in einer anderen Übersetzungseinheit abhängt, kann das zu dem Static Initialization Order Fiasco führen.
Bevor ich über die Lösung schreibe, möchte ich dir das Problem in Aktion zeigen.
Eine 50:50-Chance, es richtig zu machen
Wie verläuft die Initialisierung der statischen Variablen? Sie erfolgt in zwei Schritten: zur Compiletime und zur Runtime. Wenn eine statische Variable während der Kompilierung nicht mit einer Konstante initialisiert, werden kann, wird sie null-initialisiert. Zur Laufzeit erfolgt nun die dynamische Initialisierung für diese statischen Elemente, die null-initialisiert wurden.
// sourceSIOF1.cpp
int square(int n) {
return n * n;
}
auto staticA = square(5);
// mainSOIF1.cpp
#include <iostream>
extern int staticA; // (1)
auto staticB = staticA;
int main() {
std::cout << '\n';
std::cout << "staticB: " << staticB << '\n';
std::cout << '\n';
}
(1) deklariert die statische Variable staticA
. Die folgende Initialisierung von staticB
hängt von der Initialisierung von staticA
ab. Allerdings wird staticB
zur Compilezeit null-initialisiert und zur Runtime dynamisch initialisiert. Das Problem ist, dass es keine Garantie dafür gibt, in welcher Reihenfolge staticA
oder staticB
initialisiert werden, weil staticA
und staticB
zu verschiedenen Übersetzungseinheiten gehören. Damit ergibt sich eine 50:50 Chance, dass staticB
0 oder 25
ist. Um dieses Problem zu demonstrieren, habe ich die Link-Reihenfolge der Objektdateien geändert. Dadurch ändert sich auch der Wert für staticB
!
Was für ein Fiasko! Das Ergebnis des Programms hängt von der Linkreihenfolge der Objektdateien ab. Wie können wir dies Problem lösen?
Das Meyers Singleton
Statische Variablen mit lokalem Geltungsbereich werden erstellt, wenn sie das erste Mal verwendet werden. Lokaler Geltungsbereich bedeutet im Wesentlichen, dass die statische Variable in irgendeiner Weise von geschweiften Klammern umgeben ist. Diese verzögerte Initialisierung ist eine Garantie, die C++98 bietet. Das Meyers Singleton basiert genau auf dieser Idee. Anstelle einer statischen Instanz vom Typ Singleton
hat es eine lokale statische vom Typ Singleton
.
// singletonMeyer.cpp
#include <iostream>
class MeyersSingleton{
private:
MeyersSingleton() = default;
~MeyersSingleton() = default;
public:
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator = (const MeyersSingleton&) = delete;
static MeyersSingleton& getInstance(){
static MeyersSingleton instance; // (1)
return instance;
}
};
int main() {
std::cout << '\n';
std::cout << "&MeyersSingleton::getInstance(): "
<< &MeyersSingleton::getInstance() << '\n';
std::cout << "&MeyersSingleton::getInstance(): "
<< &MeyersSingleton::getInstance() << '\n';
std::cout << '\n';
}
static MeyersSingleton instance
in (1) ist eine statische Variable mit lokalem Geltungsbereich. Folglich ist sie verzögert initialisiert und kann nicht dem Fiasko der statischen Initialisierungsreihenfolge zum Opfer fallen. Mit C++11 wird das Meyers Singleton noch mächtiger.
Concurrency
Mit C++11 werden statische Variablen mit lokalem Geltungsbereich auch Thread-sicher initialisiert. Das bedeutet, dass das Meyers Singleton nicht nur das Static Initialization Order Fiasco löst, sondern auch garantiert, dass das Singleton Thread-sicher initialisiert wird. Außerdem ist die Verwendung einer lokalen statischen Variable für die thread-sichere Initialisierung eines Singleton der einfachste und schnellste Weg. Ich habe bereits zwei Artikel über die thread-sichere Initialisierung des Singleton geschrieben:
Wie geht's weiter?
Das Singleton-Muster ruft viele Emotionen hervor. Mein englischer Artikel "Thread-Safe Initialization of a Singleton" wurde bisher mehr als 300.000 Mal gelesen. Deshalb möchte ich in meinen nächsten Artikeln die Vor- und Nachteile des Singletons und mögliche Alternativen vorstellen. (rme)