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 Standardkonstruktor 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 Destruktoraufruf 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 Standardkonstruktors: 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 Singletonelternklasse 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
Beispielimplementierung 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
Instanziierungsmethode 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 Beispielimplementierung 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 Destruktoraufrufs fällt zusammen mit den Destruktoraufrufen 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
Standardbibliothek 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 Fremdobjekte 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 Initialisierungsreihenfolge
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
Instanzzeigers 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
Singletonimplementierungen 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 Singletonbasisklasse 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 Singletonimpementierung 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 Elternklassentemplates vorkommen (gerade das ist die problematische
Stelle, die bei einer analogen Verwendung der Wächterimplementierung 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 Singletonimplementierungen 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 Fragestellungen 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