SegFault

Design Patterns in C++ | Singleton

July 26 , 2018

Eines der wichtigsten Designpatterns ist das Singleton. Ein Singleton stellt sicher, dass maximal eine Instanz einer Klasse existiert und stellt einen globalen Zugangspunkt für diese Instanz bereit.

Vorteile

  • Einfache Anwendung. Es ist mit geringem Aufwand verbunden eine Singletonklasse zu schreiben und zu verwenden.

Vergleicht man Singletons mit globalen Variablen, welche die wohl naheliegendste Alternative darstellen, so ergeben sich einige weitere Vorteile:

  • Spezialisierung. Ein Singleton kann durch Interfaces und Unterklassen spezialisiert werden und welche Spezialisierung verwendet wird kann zur Laufzeit entschieden werden.
  • Lazyloading. Wird ein Singleton nie verwendet muss es auch nicht instanziert werden.
  • Zugriffskontrolle. Die Verwendung des Singletons kann von Bedingungen abhängig gemacht werden.
  • Mehr Ordnung. Verwendet man Singletons zusammen mit Interfaces, so lassen sich Implementierungsinterna besser kapseln.

Nachteile

Diesen Vorteilen gegenüber stehen jedoch eine Vielzahl von Nachteilen welche eine Verwendung von Singletons problematisch erscheinen lassen:

  • Exzessive Verwendung führt zu einem Konstrukt das kaum besser ist als globale Variablen.
  • Abhängigkeiten werden verschleiert, da sie nicht mehr im Interface einer Klasse auftauchen.
  • Der Scope eines Singletons lässt sich schlecht kontrollieren, werden DLL's geladen wirds noch unübersichtlicher.
  • Testfälle werden enorm verkompliziert, da sich ein Singleton während der Testsitzung kaum zurücksetzen lässt.
  • Konfiguration ist zwar theoretisch möglich, in der Praxis jedoch relativ schwierig und häufig nur über Umgebungsvariablen/Dateien möglich.
  • Das static initialization order 'fiasco' wird nur scheinbar verbessert und verwandelt sich zum Teil in ein deinitialization order 'fiasco'.

Da die Nachteile häufig überwiegen stellt das Singleton für viele, darunter mich, eher ein Antipattern dar. Nichtsdestotrotz findet es sich in vielen Anwendungen und hat in gewissen Fällen auch seine Daseinsberechtigung.

Naive Implementierung

Eine sehr einfache Implementierung welche immer wieder auf verschiedenen Seiten zu finden ist sieht wie folgt aus:

#include <iostream>

class singleton {
    singleton() {}
    singleton(const singleton&) = delete;
    singleton& operator=(const singleton&) = delete;
    public:
    void test() { std::cout << "Hallo Welt" << std::endl; }
    static singleton& get_instance() {
        static singleton instance;
        return instance;
    }
};

int main() {
	singleton::get_instance().test();
}

Auch wenn diese Implementierung scheinbar problemlos funktioniert so entstehen dennoch Probleme sobald man versucht das Singleton in anderen Objekten mit statischer Lebenszeit zu verwenden (z.b. einem anderen Singleton). C++ garantiert die Lebensdauer der statischen Variable "instance" vom ersten Aufruf der Funktion bis zum Ende der Main Funktion. Wird nun ein zweites Singleton erstellt welches unser erste Singleton im Destruktor verwendet so ist nicht garantiert welches Singleton zuerst zerstört wird und die Referenz auf unsere Instance ist unter Umständen nicht mehr gültig was zu Fehlern führen kann. Auch das holen einer Referenz im Konstruktor und zugreifen mit Hilfe dieser hilft nicht, da Referenzen in C++ (meistens*) keinen Einfluss auf die Lebensdauer eines Objektes haben.

Bessere Implementierung

Eine Lösung für dieses Problem bietet die Verwendung von std::shared_ptr.

#include <iostream>
#include <memory>

class singleton {
    singleton() {}
    singleton(const singleton&) = delete;
    singleton& operator=(const singleton&) = delete;
    public:
    void test() { std::cout << "Hallo Welt" << std::endl; }
    static std::shared_ptr<singleton> get_instance() {
        static auto instance = std::shared_ptr<singleton>(new singleton());
        return instance;
    }
};

int main() {
	singleton::get_instance()->test();
}

An der Tatsache dass Aufrufe zu singleton::get_instance() im Destruktor eines anderen statischen Objektes nicht zulässig sind ändert zwar auch die Verwendung von shared_ptr nichts, jedoch erlaubt uns shared_ptr get_instance im Konstruktor aufzurufen und das Ergebnis in einer Variable zu speichern. Die Instanz unseres Singletons bleibt dadurch gültig solange noch gespeicherte Pointer existieren.

* Es gibt Ausnahmen von dieser Regel, jedoch greift keine davon in unserem Anwendungsfall.