Patterns in der Softwareentwicklung: Das Singleton-Muster

Das Singleton Pattern gilt als das umstrittenste Entwurfmuster aus dem klassischen Buch "Design Patterns".

Lesezeit: 6 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 184 Beiträge

(Bild: Blackboard/Shutterstock.com)

Von
  • 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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das Singleton Muster ist ein Erzeugungsmuster. Hier sind die Fakten kurz und bündig:

  • Sichert zu, dass nur eine Instanz einer Klasse existiert
  • Man braucht Zugang zu einer gemeinsamen Ressource und
  • diese gemeinsame Ressource darf nur einmal vorhanden sein.
  • 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
{
  ...
};

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++.

In den folgenden Zeilen diskutiere ich verschiedene Implementierungsvarianten des Singleton. Beginnen möchte ich mit der klassischen Implementierung des Singleton-Musters.

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.

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?

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:

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)