Homeoop-trainer.de
zurückRalf Schneeweiß

Das Singleton Muster in C++

Einige Überlegungen zur Implementierung


In dem Buch „Design Patterns. Elements of Reusable Objectoriented Software.“1) haben die Autoren Erich Gamma, Richard Helm, John Vlissides und Ralph Johnson ein strukturell sehr einfaches und inzwischen weit bekanntes Muster beschrieben: das Singleton. Die Implementierungssprache, die für die Beispiele gewählt wurde, ist C++. Das legt natürlich die Verwendung dieses Musters in der genannten Sprache nahe, auch wenn gerade der Einsatz des Singletons in C++ von einigen strukturellen Problemen begleitet wird, wie in diesem Artikel zu zeigen sein wird. Im Gegensatz zu einem Einsatz des Singletons in Java, müssen Rahmenbedingungen genauer geprüft und Ziele exakter definiert werden, wenn man das Muster in C++ gewinnbringend verwenden möchte. Zunächst kann man natürlich versucht sein, den Beispielcode aus [GoF95] für eigene Projekte nutzbar zu machen. Dazu sollte man diesen allerdings etwas genauer unter die Lupe nehmen.

class Singleton
{
   public:
     static Singleton* exemplar();

   protected:
     Singleton() {}

   private:
     static Singleton *instanz;
};

Singleton* Singleton::instanz = 0;

Singleton* Singleton::exemplar()
{
  if( instanz == 0 )
    instanz = new Singleton();
  return instanz;
}
Listing 1: Singleton Pattern aus [GoF95]

In Listing 1 steht eine dem Buchbeispiel analoge Implementierung. Der Sinn des Singletonmusters besteht in erster Linie darin, nur eine Instanz einer Klasse zuzulassen. Prüfen wir die Implementierung auf Tauglichkeit vor dieser Anforderung, so fallen Lücken auf. Zum einen hat der Standard­konstruktor eine protected-Sichtbarkeit, was dazu führen kann, dass man durch einfaches Ableiten die Sichtbarkeit des Konstruktors umbeabsichtigt öffnet. Zum anderen wurde der Kopierkonstruktor ganz vergessen. Das heißt, dass der Compiler automatisch einen solchen generiert - mit der Sichtbarkeit public. Mit dem Kopierkonstruktor kann man also eine weitere Instanz erzeugen, ohne dass man durch die Struktur des Buchbeispiels daran gehindert werden würde. Will man also die Klasse gemäß der Idee des Singletons schützen, so sind Modifikationen nötig.
Eine weitere Auffälligkeit ist die Verwendung des new-Operators. Dieser wird gebraucht, um die Objektinstanz dynamisch auf dem Heap anzulegen. Andernfalls könnte man eine Instanz auch als statisches Objekt anlegen. In vielen Fällen wird das auch die sinnvollere Vorgehensweise darstellen. Die dynamische Instanziierung macht vor allem dadurch Sinn, dass nicht in jedem Fall instanziiert werden muss. Wenn das Objekt sehr groß ist oder Ressourcen gebraucht werden, die nicht in jedem Programmlauf benötigt werden, ist die Instanziierung durch new einer statischen vorzuziehen. Dabei stellt sich aber die Frage nach dem Löschen des Objektes. In dem vorgebrachten Beispiel ist kein struktureller Bestandteil enthalten, der sich um das Beseitigen der Instanz kümmert. Wer nun nicht darauf vertrauen möchte, dass das Betriebssystem den Speicher schon wegräumen werde2), und wer einen Destruktor­aufruf der Objektinstanz braucht, um andere Ressourcen freizugeben, der wird sich eine Lösung überlegen müssen. Die Lösung bedeutet eine strukturelle Änderung wenn sie systemimmanent3) sein soll.
Nach diesen Vorüberlegungen zu dem von den [GoF95] gegebenen Implementierungsbeispiel ist es nun möglich erste Verbesserungsvorschläge zu erarbeiten, zunächst ohne dabei auf besondere Ausprägungen des Musters einzugehen, wie sie beispielsweise in der Publikation von J. Vlissides diskutiert werden (dort wird das Singleton im Zusammenhang mit Referenzzählung betrachtet und fortentwickelt).

class Singleton
{
   public:
     static Singleton* exemplar();

   protected:
     Singleton() {}

   private:
     static Singleton *instanz;
     Singleton( const Singleton& );
};

...
Listing 2: Singleton mit privatem Kopierkonstruktor

Das am einfachsten zu lösende Problem ist das mit dem Kopierkonstruktor: man muss ihn einfach nur mit privater Sichtbarkeit deklarieren. Privat deklariert ist der Kopierkonstruktor nicht mehr vom Compiler generierbar und kann auch nicht mehr aufgerufen werden. Eine Instanz läßt sich durch ihn nicht mehr erstellen.
Etwas komplexer verhält es sich mit der Sichtbarkeit des Standard­konstruktors: die protected-Sichtbarkeit, wie sie bei [GoF95] angegeben ist, deutet auf eine gewollte Vererbbarkeit der Singletonklasse hin. Damit muss auch die Singletoneigenschaft vererbbar sein. Betrachtet man die aus einer Vererbung resultierenden Kindklassen, so stellt man fest, dass die statischen Elemente der Singleton­elternklasse nur einmal für alle abgeleiteten Klassen gemeinsam existieren. Damit wird also nicht ein Verhalten realisiert, das gewährleistet, dass die Kindklassen jeweils nur einmal instanziiert werden können, sondern ein Verhalten, das nur eine Instanz irgend einer abgeleiteten Klasse erlaubt. Das ist ein ziemlich bemerkenswertes Verhalten das, wenn es gewünscht wird, in der vorliegenden Beispiel­implementierung mit dem geschützten Standardkonstruktor fast gelöst ist. Um das Problem vollständig zu lösen, muss für eine solche Kindklasse noch die Instanziierungs­methode neu geschrieben werden und der Standardkonstruktor versteckt werden (und schon findet man fast die ganze Singletonstruktur in der Kindklasse wieder). Der Sinn einer solchen Elternklasse mit dem angesprochenen Verhalten ist also äußerst zweifelhaft. Meint man jedoch, mit der Vererbung Kindklassen erzeugen zu können, die eine proprietäre Singletoneigenschaft aufweisen, so taugt die Beispiel­implementierung nicht. Es muss also gefragt werden, ob für eine Sigletonimplementierung die angesprochene Eigenschaft überhaupt wünschenswert ist. Wenn nicht - was in den meisten Fällen zutrifft -, dann sollte der Konstruktor die private Sichtbarkeit bekommen, um eine Vererbung unmöglich zu machen und es muss der fachliche Code mit dem Mustercode gemischt werden. Diese nicht sehr elegente Variante ist zumindest sicher und garantiert die Singletoneigenschaft ohne sie durch eine mögliche Vererbung aufzubrechen.

class Singleton
{
   public:
     static Singleton* exemplar();
     
     void fachlicheFunktion1();
     void fachlicheFunktion2();
     ...

   private:
     static Singleton *instanz;

     Singleton() {}
     Singleton( const Singleton& );
};

...
Listing 3: Singleton mit privatem Standardkonstruktor

Will man allerdings eine allgemeine Klasse definieren, die als Singletonelternklasse die Singletoneigenschaft proprietär auf ihre Kindklassen vererbt, muss man andere Wege gehen, die an einer späteren Stelle in diesem Artikel erörtert werden sollen. Zunächst soll auf das Problem des Löschens eingegangen werden:
Listing 1 zeigt die statische Methode, die die Instanz der Klasse erzeugt. Dort wird die Instanz mittels new auf dem Heap angelegt. Alternativ könnte auch eine statische Instanz angelegt werden, deren Referenz - oder deren Zeiger - die Methode zurückgibt. Die Konsequenz ist allerdings, dass schon zur Startzeit des Prozesses der Speicher vorreserviert wird (was in vielen Fällen vertretbar ist). Größere Speichermengen können ja dynamisch im Konstruktor der Klasse angefordert werden. Auch in dieser Implementierung kann der fachliche Code untergemischt werden. Die Instanz wird beim Prozessende ordentlich beseitigt und es wird der Destruktor aufgerufen. Für diesen ist allerdings eine Rahmenbedingung wirksam: der Zeitpunkt des Destruktor­aufrufs fällt zusammen mit den Destruktor­aufrufen aller anderen globalen Instanzen des Prozesses. Sollte nun ein Design generell so konzipiert sein, dass man möglichst keine globalen Instanzen irgendwelcher Klassen benutzt, so sind doch einige globale Objekte aus der Standard­bibliothek vorhanden, die ebenfalls abgebaut werden. In welcher Reihenfolge diese Objekte nun beseitigt werden ist nicht festgelegt. Aus diesem Grund darf der Destruktor der Singletonklasse auf keine globalen Fremd­objekte mehr zugreifen, denn diese könnten ihrerseits bereits abgeräumt sein. Kann man die Rahmenbedingung erfüllen und bereitet das Vorreservieren des Speichers keine Probleme, so ist diese sicherlich die unproblematischste aller Singletonimplementierungen.

class Singleton
{
   public:
     static Singleton& exemplar();

   private:
     Singleton() {}
     Singleton( const Singleton& );
};

Singleton& Singleton::exemplar()
{
  static Singleton instanz;
  return instanz;
}
Listing 4: Singleton mit statischer Instanz

In dem Fall wenn die Instanz aus Platzgründen nicht statisch angelegt werden kann und new verwendet werden soll, muss eine andere Möglichkeit zum Abräumen der Singletoninstanz geschaffen werden. Das kann beispielsweise durch ein statisches Wächterobjekt geschehen, das minimalen Platz benötigt und bei Prozessende, im Falle der Instanziierung des Singletons, die Instanz löscht. Damit dieses Objekt an der Stelle gekapselt ist, wo es benötigt wird, kann man dessen Klasse als verschachtelte Klasse privat innerhalb des Singletons deklarieren. Damit wird auch das Problem der undefinierten Initialisierungs­reihenfolge der globalen Objekte umgangen. Die Singletoninstanz wird, obwohl sie statisch ist, nach den globalen Objekten initialisiert. Auch hier gilt: der Destruktor der Singletonklasse wird durch den Destruktor der statischen Wächterinstanz aufgerufen, also während der Aufräumarbeiten aller globalen Objektinstanzen. Da die Reihenfolge der Objektlöschungen nicht festgelegt ist, kann auf kein anderes globales Objekt zu diesem Zeitpunkt zugegriffen werden. Die vorgestellte Lösung mit der Wächterinstanz hat gegenüber der Lösung mit der statischen Instanz also nur den Vorteil, dass während der Laufzeit Ressourcen gespart werden können, wenn die Singletoninstanz gerade nicht benötigt wird und bei einer Instanziierung viele Ressourcen verbrauchen würde. In Listing 5 finden Sie die Lösung mit der Wächterinstanz.
Die friend-Stellung der inneren Wächterklasse ist für einige Compiler notwendig. Andernfalls erlauben sie keinen Zugriff auf die privaten Elemente der umgebenden Klasse. Bei einigen Compilern zieht auch die Instanziierung der Wächterinstanz, wie sie im Listing dargestellt ist, eine Warnung nach sich, denn es wird ein Bezeichner eingeführt, auf den niemals zugegriffen wird. Dem kann natürlich durch entsprechende Compilerpragmas oder Warninglevels Abhilfe geschaffen werden. Wenn man eine compilerübergreifende Lösung haben möchte kann man eine leere Inlinemethode in der Wächterklasse definieren, die nach der Zeile mit der statischen Instanziierung aufgerufen werden kann. Die Compiler sind heute fast alle in der Lage, eine solche leere Inlinemethode wegzuoptimieren. Die Warnung ist damit strukturell beseitigt.
Da nun aber das Löschen exklusiv durch das Singleton selbst geschehen soll, sollte man den eigentlichen Destruktor privat machen. Damit wird das Löschen ausserhalb des Scopes der Singletonklasse unterbunden.

class Singleton
{
   public:
     static Singleton* exemplar();

   private:
     static Singleton *instanz;
     Singleton() {}
     Singleton( const Singleton& );

     ~Singleton() {}

     class Waechter {
         public: ~Waechter() {
           if( Singleton::instanz != 0 )
             delete Singleton::instanz;
         }
     };
     friend class Waechter;
};

Singleton* Singleton::instanz = 0;

Singleton* Singleton::exemplar()
{
  static Waechter w;
  if( instanz == 0 )
    instanz = new Singleton();
  return instanz;
}
Listing 5: Singleton mit Wächterinstanz

Wenn eine Variante mit dem new-Operator in einem Kontxt implementiert werden soll, der mehrere Threads nutzt, muss man die Instanziierung des Objekts durch eine Mutex schützen. Andernfalls ist es möglich, dass zwei Objekte des Singletons in unterschiedlichen Threads angelegt werden, wenn ein Kontextwechsel durch den Scheduler nach der Abfrage des Instanz­zeigers durchgeführt wird. Da Mutexe systemspezifisch sind, wird hier keine Implementierung gezeigt.

Im nächsten Abschnitt wird auf das Problem einzugehen sein, dass die bis jetzt entwickelten Singleton­implementierungen ein proprietäres Vererben der Singletoneigenschaft auf Kindklassen nicht erlauben. Zur Erinnerung: die statischen Elemente einer Elternklasse werden durch Kindklassen gemeinsam genutzt. Will man erreichen, dass für jede Kindklasse eigene statische Elemente zur Verfügung stehen, so muss für jede dieser Klassen eine exklusive Singleton­basisklasse existieren. Wenn man das von Hand erreichen wollte, wäre das ein enormer zusätzlicher Schreibaufwand, der den Einsatz der Musterlösung an sich in Frage stellen würde.
Zu Hilfe kommt hier die Möglichkeit in C++, durch Templates den Compiler die Klassen erzeugen zu lassen: man kann eine Elternklasse in Abhängigkeit von der Kindklasse erzeugen lassen. Dabei kann man auch gleich der statischen Instanziierungsmethode den gewünschten Produkttyp einschieben.
Realisierbar ist diese Lösung sowohl mit dem Grundgerüst der Implementierung mit der Wächterinstanz als auch mit der auf der statischen Seingletoninstanz beruhenden Implementierung. Für eine Demonstration wird hier die letztgenannte gewählt, da sie etwas einfacher ist und mit weniger Zeilen Code auskommt. Die Variante mit Wächterinstanz läßt sich analog für eine Template-Implementierung verwenden. Allerdings können bei einigen Compilern Probleme mit der Übersetzbarkeit auftreten, wenn diese den ANSI/ISO-C++-Standard nicht korrekt umsetzen. Um diese Probleme im Einzelfall zu umgehen, und um die Codegröße auf ein Minimum zu reduzieren wählte ich für die Beispielimplementierung in Listing 6 die Variante mit statischer Instanziierung.
Da die Singleton­impementierung nun eine Elternklasse sein soll, die ihre Eigenschaften auf die Kindklassen vererbt, muss der Standardkonstruktor public oder protected sein. Was sich strukturell ändert, ist nur der Einschub der abgeleiteten Klasse als Produkttyp in die Instanziierungsmethode und die Vervielfältignug dieser Methode. Etwas seltsam mag dem einen oder anderen auch die Verwendung des Kindklassentyps zur Parametrisierung des Elternklassen­templates vorkommen (gerade das ist die problematische Stelle, die bei einer analogen Verwendung der Wächter­implementierung zu Compilerfehlern führen kann, da dort stärker auf Typinformationen der Kindklasse Bezug genommen werden muss). Eine solche Verwendung des Kindklassentyps ist jedoch unproblematisch, da Informationen über den inneren Aufbau der Klasse nur in der Instanziieringsmethode gebraucht werden. Diese ist aber unabhängig von einem Objekt der Elternklasse.

template <class Derived>
class Singleton
{
   public:
     static Derived& exemplar();

   protected:
     Singleton() {}

   private:
     Singleton( const Singleton& );
};

template <class Derived>
Derived& Singleton<Derived>::exemplar()
{
  static Derived instanz;
  return instanz;
}
Verwendung:
class ChildA : public Singleton<ChildA>
{  ...  };

class ChildB : public Singleton<ChildB>
{  ...  };
Listing 6: Template-Implementierung der Elternklasse(n)

Ein Problem, das wir mit dieser auf Templates basierten Implementierung haben ist, dass die Kontruktoren der Kindklassen nicht mehr verborgen sind. Wenn man sie verbergen möchte, müsste man für die Elternklasse eine friend-Deklaration einführen. Das kann z. B. über ein Makro geschehen oder eben direkt. Das bedeutet aber zusätzlichen Aufwand, der die Eleganz der Lösung in Frage stellt. Die erzeugten Kindklassen haben also alle Elemente eines Singletons mit Ausnahme der verborgenen Konstruktoren. Mit entsprechender Zusatzinformation lassen sie sich aber verwenden (eine Instanz darf nur über die Instanziierungsmethode angefordert werden).
Als Letztes möchte ich auch dafür noch einen Vorschlag machen: die unproblematischen Singleton­implementierungen sind die, die ihre Musterstruktur mit dem fachlichen Code mischen ohne dabei Vererbung einzusetzen. Die Elemente des Musters können also alle mittels eines Makros in eine Klasse eingefügt werden. Das Makro kann die Klassenelemente schon mit der richtigen Sichtbarkeit deklarieren. Für die Definition in der statischen Elemente ausserhalb der Klassen ist dann ein weiteres Makro zuständig. Das könnte etwa so aussehen, wie es in Listing 7 demonstriert wird. Die Makros müßten nur die Deklarationen bzw. die Definitionen einer der beiden Mustervarianten (Wächterinstanz in Listing 4 oder statische Singletoninstanz in Listing 5) wiederholen. Auf die Implementierung der Makros wird an dieser Stelle verzichtet, da sie mehr oder weniger trivial ist. Der Vorteil dieser Lösung liegt in seiner einfachen Handhabung. Der Compiler gibt Fehlermeldungen aus, wenn man das implementierende Makro vergisst anzugeben.

class MyClass
{
    DECLARE_SINGLETON(MyClass);

    void fachlicheFunktion1();
    void fachlicheFunktion2();
    ...
};

...

DEFINE_SINGLETON(MyClass);
Listing 7: Verwendung eines Singletons auf Makrobasis

In diesem Artikel wurden weder besondere Ausprägungen des Singletonmusters beschrieben, noch Frage­stellungen des Softwaredesigns angeprochen, die für oder gegen die Verwendung von Singletons sprechen. Es sollte nur aufgezeigt werden wie komplex sich die Frage nach der richtigen Implementierung des scheinbar strukturell so einfachen Musters stellt. In Gesprächen mit Entwicklern, die sich erst in die Musterproblematik einarbeiten oder erst einen oberflächlichen Kontakt mit dieser hatten, stellte ich oft fest, dass das Singletonmuster vor allen anderen als bekannt angegeben wurde. Dabei ist es ein Muster, das große Probleme für das Design und für die Implementierung - wie hier gezeigt wurde - nach sich zieht.
Was in diesem Artikel ausgelassen wurde, sind insbesondere die Fragen der genauen Lebenszeit des Singletonobjektes. Der Autor des Buches „Modern C++ Design“, Andrei Alexandrescu, setzt sich mit dieser Problematik genauer auseinander, denn es ist eine weitere notwendige Dimension bei den Überlegungen zum Einsatz von Singletons.
Die Verwendung dieses Musters sollte also gut überlegt sein. In den meisten Fällen, in denen ich Singletonimplementierungen in Projekten antraf, warfen diese Probleme auf und wären durch andere Konstrukte problemlos ersetzbar gewesen. Die wenigen Zeilen des Musterbeispiels und die vermeintlich einfache Struktur haben einen verführerischen Charakter. Das Singleton ist eine Lösung für ein exklusives, selten auftretendes Problem und sollte daher ebenso exklusiv und selten verwendet werden.

Ralf Schneeweiß - 18. September 2003


1)

Im Folgetext einfach [GoF95] genannt - von „Gang of Four“.

2)

Dieses Thema taucht des öfteren in einschlägigen Diskussionsforen auf. Dabei wird häufig mit den Fähigkeiten des Betriebssystems argumentiert, Speicher freizugeben, wenn der Prozess beendet wird. Ohne dabei für Einzelfälle ein Urteil zu fällen, muss doch darauf hingewiesen werden, dass nur ein Teil der möglichen Plattformen eine virtuelle Speicherverwaltung besitzt. Außerdem ist es eine externe Rahmenbedingung, die nicht immanent als Voraussetzung eines Designs betrachtet werden kann. Aus diesem Grund wird von dem Autor des Textes das Belassen einer Instanz über das Prozessende hinaus als grundsätzlich negativ bewertet.

3)

Natürlich kann am Ende eines Prozesses explizit eine Funktion aufgerufen werden, die Aufräumarbeiten verrichtet und dabei eine Singletoninstanz löscht. Das wäre aber eine Lösung ausserhalb der Musterstruktur und wenig elegant.


Literatur

[GoF95] 

Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Elements of Reusable Objectoriented Software. 1995.

[JV99] 

John Vlissides: Entwurfsmuster anwenden. 1999.

[AA01] 

Andrei Alexandrescu: Modern C++ Design. Generic Programming and Design Patterns Applied. 2001.

Anhang

Die gezeigten Beispiele wurden mit den folgenden Compilern getestet:

  • GCC C++ 3.3
  • Borland C++ 5.5.1
  • Digital Mars C++ 8.29n



zurück Zum Anfang des Dokuments. Home