Einsam und allein

Für bestimmte Anwendungsfälle braucht man Objekte, die es im ganzen Programm nur einmal geben kann. Das passende Programmiermuster dafür nennt sich "Singleton". Um das zu bauen sind static-Felder unerlässlich.

Video

Quelltext

#include <iostream>

class PrinterPtrSingleton
{
    static PrinterPtrSingleton *sp;

    PrinterPtrSingleton() 
    {
        std::cerr << "ptr Initialisierung\n";
    }
    
    ~PrinterPtrSingleton()
    {
        std::cerr << "ptr Destruktor\n";
    }
    
    PrinterPtrSingleton(PrinterPtrSingleton const &) = delete;
    
    PrinterPtrSingleton &operator=(PrinterPtrSingleton const &) = delete;
    
public:
    
    static PrinterPtrSingleton &getInstance()
    {
        if (sp == nullptr) {
            sp = new PrinterPtrSingleton;
        }
        
        return *sp;
    }
    
    void print(std::string const &msg)
    {
        std::cout << "Nachricht: " << msg << std::endl;
    }
};

PrinterPtrSingleton *PrinterPtrSingleton::sp;

class PrinterLocalSingleton
{
    PrinterLocalSingleton() 
    {
        std::cerr << "local Initialisierung\n";
    }
    
    ~PrinterLocalSingleton()
    {
        std::cerr << "local Destruktor\n";
    }
    
    PrinterLocalSingleton(PrinterLocalSingleton const &) = delete;
    
    PrinterLocalSingleton &operator=(PrinterLocalSingleton const &) = delete;
    
public:
    
    static PrinterLocalSingleton &getInstance()
    {
        static PrinterLocalSingleton s;
        return s;
    }
    
    void print(std::string const &msg)
    {
        std::cout << "Nachricht: " << msg << std::endl;
    }
};

int main()
{
    std::cerr << "main betreten\n";
    PrinterPtrSingleton::getInstance().print("Hallo!");
    
    PrinterLocalSingleton::getInstance().print("Hallo nochmal!");
    PrinterPtrSingleton::getInstance().print("Hallo!");
    
    PrinterLocalSingleton::getInstance().print("Hallo nochmal!");
    std::cerr << "main verlassen\n";
}

Erklärung

Das Beispiel zeigt zwei verschiedene mögliche Realisierungen des Singletons, die sich leicht unterschiedlich verhalten. Die Grundidee der Singletons ist immer gleich: alle Konstruktoren und Kopiermöglichkeiten (wie bspw. Copy-Assignment) werden private gemacht oder ganz gelöscht, um versehentliche Kopien des Singletons zu vermeiden. Der Zugriff auf das Singleton-Objekt erfolgt über eine getInstance()-Methode, der allein die Instanziierung obliegt. Im Falle des PrinterPtrSingleton geschieht das, indem diese Methode überprüft, ob bereits ein Objekt angelegt wurde (dann ist der static-Pointer in der Klasse ungleich nullptr. Die Tatsache, dass der auch ohne explizite Initialisierung 0 ist, wird für static-Felder vom C++-Standard garantiert). Ist das nicht der Fall, dann legt die Methode ein neues Objekt an. Für alle folgenden Aufrufe wird das bestehende Objekt zurückgeliefert. Bei der Implementierung des PrinterLocalSingleton machen wir uns die Tatsache zunutze, dass static-Variablen auch lokal in einer Funktion sein können. Hier garantiert der C++-Standard, dass die Initialisierung der Variable beim ersten Durchlaufen des betreffenden Codes stattfindet und danach übersprungen wird. Daher ist die Implementierung von getInstance() so wesentlich kürzer. Außerdem haben wir durch die Vermeidung von new hier den Vorteil, dass das Singleton-Objekt nach dem Verlassen von main() weggeräumt wird (wie an der Ausgabe des Destruktors zu sehen). Für die Variante mit dem Pointer müssten wir das selbst tun (was wir hier nicht tun, da es schwierig ist, zu wissen, wann wir das wegräumen können). Theoretisch könnten wir noch eine dritte Variante implementieren: statt eines Pointers als static-Klassenmember könnten wir direkt das Objekt anlegen. Damit würde sich allerdings die Initialisierungszeit verändern: dieses Objekt wird vor dem Betreten von main() initialisiert. Je nach Anwendungsfall kann das problematisch sein (bspw. wenn wir Informationen von der Kommandozeile zur Initialisierung brauchen).

Insgesamt sieht auf Anhieb die PrinterLocalSingleton-Variante am einfachsten aus. Aber auch die hat so ihre Tücken. Spätestens, wenn man dieses Singleton mit mehreren Threads im Programm verwendet, kann es hässlich werden. Dann kann es nämlich dazu kommen, dass es doch zweimal initialisiert wird. Wie man sich dagegen schützt, ist allerdings ein anderes Thema.