Mit der erweiterten Funktionalität zur Initialisierung von Objekten und den expliziten Klassendefinitionen durch neue Schlüsselwörter kann C++11 in zwei Aspekten deutlich punkten: Zum einen lässt sich Lebenszeit eines Objekts einfacher und expliziter steuern, zum anderen drücken Klassendefinitionen rein deklarativ ihre Intention aus und sind damit viel leichter wartbar.
Mit Initialisiererlisten für Konstruktoren, der Delegation und Vererbung von Konstruktoren und auch dem direkten Initialisieren von Klassenelementen wird die Initialisierung von Objekten in C++11 deutlich erweitert.
Sequenzkonstruktor
Konstruktoren, die ein Template vom Typ std::initializer_list
annehmen, werden Konstruktoren mit Initialisiererliste genannt. Sie werden auch als Sequenzkonstruktor bezeichnet. Der Sequenzkonstruktor ist die Grundlage dafür, dass sich die STL-Container-Strings direkt über Initialisiererlisten wie C-Aggregate initialisieren lassen. Nicht nur die neuen Konstruktoren, auch Zuweisungsoperatoren unterstützen die neue Syntax. Dabei besitzt das äußerst praktische Template std::initializer_list
, das unter der Decke ein Array ist, nur drei Methoden (Tabelle 7.1):
Methode | Beschreibung |
| Zeiger auf das erste Element des Arrays. |
| Zeiger auf eine Position hinter dem letzten Element des Arrays. |
| Anzahl der Elemente des Arrays. |
Aus diesem einfachen Interface ist unmittelbar ersichtlich: Initialisiererlisten können nicht modifiziert werden. Die drei Methoden sind ausreichend, um einen std::vector
über eine Initialisiererliste zu initialisieren:
01 template<typename T> 02 class vector { 03 public: 04 vector (std::initializer_list<T> inList){ 05 reserve(inList.size()); 06 uninitialized_copy(inList.begin(),inList.end(), elem); 07 sz = inList.size(); 08 } 09 // the rest 10 11 // ... 12 };
In Listing 7.1 wird der std::vector
über die Initialisiererliste inList
(Zeile 4) initialisiert. Die Methode reserve
(Zeile 5) stellt ausreichend Speicher zur Verfügung, um mit der Methode unitialized_copy
(Zeile 6) elem
mit den Elementen von inList
zu initialisieren. sz
merkt sich abschließend die Anzahl der Elemente von inList
(Zeile 7).
Überladen von Konstruktoren
Konstruktoren werden in der Regel überladen. Hierzu gilt es, ein paar Regeln im Gedächtnis zu behalten, wenn ein Datentyp sowohl einen Sequenzkonstruktor als auch klassische Konstruktoren besitzt.
Im Konstruktoraufruf wird eine Initialisiererliste verwendet.
Falls sowohl der Sequenzkonstruktor als auch der klassische Konstruktor angewandt werden kann, wird der Sequenzkonstruktor vorgezogen.
Falls der Sequenzkonstruktor nicht angewandt werden kann (z. B. Typinkompatibilität), werden die klassischen Konstruktoren angewendet.
Im Konstruktoraufruf wird keine Initialisiererliste genutzt.
Der Sequenzkonstruktor wird nicht verwendet.
Initialisiererlisten bei benutzerdefinierten Typen setzen einen definierten Sequenzkonstruktor voraus.
Falls für den Datentyp kein Konstruktor definiert ist, ist die Verwendung einer Initialisiererliste nur dann zulässig, wenn dieser ein Aggregat oder ein Built-in-Typ ist.
Listing 7.2 soll die Theorie mit der Praxis verknüpfen.
initializerListOverload.cpp
01 #include <initializer_list> 02 #include <iostream> 03 04 class MyData{ 05 public: 06 07 MyData(std::string,int){ 08 std::cout << "MyData(std::string,int)" << std::endl; 09 } 10 11 MyData(int,int){ 12 std::cout << "MyData(int,int)" << std::endl; 13 } 14 15 MyData(std::initializer_list<int>){ 16 std::cout << "MyData(std::initializer_list<int>)" << std::endl; 17 } 18 }; 19 20 int main(){ 21 22 std::cout << std::endl; 23 24 // sequence constructor has a higher priority 25 MyData{1,2}; 26 27 // invoke the classical constructor explicitly 28 MyData(1,2); 29 30 // use the classical constructor 31 MyData{"dummy",2}; 32 33 // still valid with C++11 34 int a{1}; 35 36 // still valid with C++11 37 int intArray[] = {1,2}; 38 39 std::cout << std::endl; 40 41 }
Listing 7.2 ist aufs Wesentliche reduziert. So besitzen die Konstruktorargumente in MyData
keinen Namen (Zeilen 7, 11 und 15), und die Objekte aus den Konstruktoraufrufen in Zeile 25, 28 und 31 werden an keine Variable gebunden. Die Ausgabe bestätigt die erweiterten Regeln zum Überladen von Konstruktoren:
MyData{1,2}
in Zeile 25 wird auf den Sequenzkonstruktor in Zeile 15 abgebildet, obwohl der klassische Konstruktor in Zeile 11 anwendbar ist. Um diesen Konstruktor aufzurufen, muss MyData(1,2)
in Zeile 31 mit runden Klammern verwendet werden. Anders verhält es sich mit dem dritten Konstruktoraufruf MyData{"dummy",2}
in Zeile 37. Da kein Sequenzkonstruktor die richtige Signatur besitzt, wird trotz Initialisiererliste der klassische Konstruktor in Zeile 7 verwendet. Sowohl der Built-in-Typ int
als auch das Aggregat intArray
kann weiterhin über Initialisiererlisten initialisiert werden.
Unterscheiden Sie die Konstruktoraufrufe des Vektors.
Unterscheiden Sie genau zwischen den beiden Konstruktoraufrufen:
std::vector<int> myVec1(10);
std::vector<int> myVec2{10};
Im ersten Fall wird ein Vektor mit zehn Elementen erzeugt, im zweiten Fall hingegen ein Vektor mit dem Element 10. Entsprechend führt die Anweisung myVec1= 5
zu einer Fehlermeldung, während nach der Anweisung myVec2= {5}
der Vektor myVec2
das Element 5 besitzt.
initListConstructor.cpp
Aufgabe 7-1
Schreiben Sie einen Datentyp, der eine Initialisiererliste von Paaren (int
,std::string
) annimmt.
Iterieren Sie im Initialisiererlisten-Konstruktor Ihres Datentyps über die Initialisiererliste und geben Sie alle Paare aus.
Aufgabe 7-2
Definieren Sie eine Initialisiererliste.
Viel hat eine std::initializer_list<std::string>
nicht zu bieten (Tabelle 7.1). Definieren Sie eine Initialisiererliste und geben Sie deren Elemente aus.
myStrangeType.cpp
Aufgabe 7-3
Erweitern Sie den Datentyp MyStrangeType
um einen klassischen Konstruktor und einen Sequenzkonstruktor.
template<typename T> class MyStrangeType{};
Dabei soll der Sequenzkonstruktor der Standardkonstruktor von MyStrangeType
sein. Entwerfen Sie den Datentyp und wenden Sie beide Konstruktoren an.
Da die Delegation von Konstruktoren recht intuitiv ist, gibt es zur Abhandlung der Delegation von Konstruktoren in Teil I, nur noch ein paar Anmerkungen.
Fertig konstruiertes Objekt
In klassischem C++ ist ein Objekt fertig konstruiert, wenn sein Konstruktor ausgeführt wurde. Dies ändert sich mit C++11. Hier gilt: Sobald der erste Konstruktor fertig ausgeführt wurde, ist das Objekt fertig konstruiert. Das bedeutet natürlich, dass jeder weitere Konstruktor auf einem fertig konstruierten Objekt agiert.
Auszeichner von Konstruktoren
Zugriffsbeschränkungen auf Konstruktoren wie public
, protected
oder private
oder auch Auszeichner von Konstruktoren wie inline
oder explicit
haben keinen Einfluss auf die Delegation von Konstruktoren.
ill-formed
Eine Gefahr muss aber im Fokus bleiben. Konstruktoren, die direkt oder indirekt rekursiv aufgerufen werden, führen zu einem ill-formed
-Programm. Ein ill-formed
-Programm ist ein Programm, das nicht der Syntax genügt. Dabei muss der Compiler nicht mal eine Warnung ausgeben. Ein Beispiel aus dem aktuellen C++11 Draft (Komitee, 2008) zur Delegation von Konstruktoren folgt:
struct C { C( int ) { } // 1: non-delegating constructor C(): C(42) { } // 2: delegates to 1 C( char c ) : C(42.0) { } // 3: ill-formed due to recursion with 4 C( double d ) : C('a') { } // 4: ill-formed due to recursion with 3 };
Aufgabe 7-4
Schreiben Sie ein Programm, das die rekursive Delegation der Konstruktoren von Listing 2.3 anwendet.
Was passiert, wenn Sie das Programm übersetzen bzw. ausführen? Diese spannende Frage lässt sich mit dem aktuellen GCC- und Clang-Compiler beantworten.
Ein einfaches using Base::Base
in Listing 7.4 genügt, und alle Konstruktoren der Klasse Base
stehen in der Klasse Derived
zur Verfügung. In Teil I, ist dieses neue Feature in Aktion zu sehen.
class Base { // ... }; class Derived: public Base{ public: using Base::Base; };
Dabei werden die Konstruktoren in der Klasse Derived
nur impliziert definiert, wenn sie auch benutzt werden.
Kopier-, Move- und Standardkonstruktor
Zwar werden alle Konstruktoren der Basisklasse in die abgeleitete Klasse vererbt, es gibt aber eine Ausnahme zu dieser Regel. Der Kopier-, der Move- und der Standardkonstruktor werden nicht geerbt. Da die drei geerbten Konstruktoren nicht als benutzerdefinierte Konstruktoren behandelt werden, erzeugt der Compiler bei Bedarf die drei Konstruktoren nach den bekannten Regeln.
Vorrang von benutzerdefinierten Konstruktoren
Besitzt ein benutzerdefinierter Konstruktor die gleiche Signatur wie ein geerbter Konstruktor, versteckt der benutzerdefinierte Konstruktor den geerbten. In Listing 7.5 wird daher der Konstruktor von Derived(int i){}
verwendet.
class Base { public: Base(int i){} }; class Derived: public Base{ public: using Base::Base; Derived(int i){} }; // ... Derived d(5);
Charakteristiken der geerbten Konstruktoren
Die abgeleitete Klasse erbt nicht nur die Konstruktoren, sondern auch deren Charakteristiken. Dies betrifft die Zugriffsbeschränkungen public
, protected
und privat
sowie deren Deklaration als explicit
- bzw. constexpr
-Konstruktor. Somit quittiert der Compiler den Aufruf Derived d(5);
in Listing 7.6 mit einer Fehlermeldung, da der Konstruktor der Basisklasse privat ist.
class Base { private: Base(int i){} }; class Derived: public Base{ public: using Base::Base; }; // ... Derived d(5);
Nicht initialisierte Variablen
Besitzt eine abgeleitete Klasse eigene Variablen, ist die Gefahr recht groß, dass diese durch die neue Syntax nicht initialisiert werden. In Listing 7.7 wird die Variable j
des Objekts d
durch den Aufruf Derived d(5)
nicht initialisiert.
class Base { private: Base(int i){} }; class Derived: public Base{ private: int j; public: using Base::Base; }; // ... Derived d(5);
Mehrfachvererbung
Eine typische Fehlerquelle bei der Mehrfachvererbung fehlt noch. Erbt eine Klasse die Konstruktoren von mehreren Klassen, sodass die erbende Klasse zwei Konstruktoren mit der gleichen Signatur einführt, führt dies zu einem Compiler-Fehler. Diese Zweideutigkeit lässt sich aber einfach auflösen, indem in der erbenden Klasse ein Konstruktor mit der gleichen Signatur definiert wird, der die geerbten versteckt. Durch den Konstruktor Derived(int i){}
wird der Aufruf Derived d(5)
gültig.
class Base1 { public: Base1(int i){} }; class Base2 { public: Base2(int i){} }; class Derived: public Base1, public Base2{ public: using Base1::Base1; using Base2::Base2; Derived(int i){} }; // ... Derived d(5);
Seien Sie sich der Gefahren des Vererbens von Konstruktoren bewusst.
Das Vererben von Konstruktoren ist ein mächtiges Feature, das aber einige neue Gefahren in sich birgt.
Abgeleitete Klassen besitzen keinen implizit definierten Standardkonstruktor.
Variablen der erbenden Klasse werden nicht initialisiert.
Mehrfachvererbung kann zu Konstruktoren mit gleicher Signatur führen.
inheritConstructor.cpp
Aufgabe 7-5
Leiten Sie public
, protected
und private
von einer Basisklasse Base
ab und verwenden Sie das Vererben von Konstruktoren.
Die geerbten Konstruktoren der Basisklasse behalten ihre Sichtbarkeit der Basisklasse. Die abgeleiteten Klassen schränken ihre Sichtbarkeit durch protected
und private
ein. Was bedeutet das für die Sichtbarkeit der geerbten Konstruktoren?
Bevor Sie das kleine Testprogramm schreiben und ausführen, überlegen Sie sich zuerst den entscheidenden Punkt: Welches Verhalten erwarten Sie?
C++11 erlaubt das direkte Initialisieren von Klassenelementen. Damit hebt C++11 die Einschränkung des klassischen C++ auf, das diese Features nur für statische, konstante Elemente integralen Typs zulässt. In Teil I, wurde diese neue Funktionalität bereits eingeführt. Was noch fehlt, ist das direkte Initialisieren von Klassenelementen für eine Klasse. Als Beispiel soll die Klasse Widget
in Listing 7.9 dienen, die sukzessive refaktoriert werden soll.
Zuerst die Ausgangsimplementierung, die ohne C++11-Feature auskommt:
class Widget{ public: Widget():height(480),width(640), frame(false),visible(true) {} Widget(int h): height(h),width(getWidth(h)), frame(false),visible(true){} Widget(int h,int w): height(h),width(w), frame(false),visible(true){} private: int height; int width; bool frame; bool visible; };
Hier bietet es sich an, die Variablen frame
und visible
direkt zu initialisieren. Damit wird die Klasse Widget
deutlich übersichtlicher (Listing 7.10).
class Widget{ public: Widget():height(480),width(640){} Widget(int h): height(h),width(getWidth(h)){} Widget(int h,int w): height(h),width(w){} private: int height; int width; bool frame= false; bool visible= true; };
Wird eine direkt initialisierte Variable auch in einem Konstruktoraufruf gesetzt, hat dieser Vorrang. Daher können die Variablen height
und width
zusätzlich direkt initialisiert werden. Damit erhalten wir die endgültige Version der Klasse Widget
in Listing 7.11.
class Widget{ public: Widget(){} Widget(int h): height(h),width(getWidth(h)){} Widget(int h,int w): height(h),width(w){} private: int height= 480; int width= 640; bool frame= false; bool visible= true; };
Soll die Klasse erweitert oder ein gemeinsamer Konstruktor identifiziert werden, an den alle anderen Konstruktoren ihre Arbeit delegieren, ist dies mit der endgültigen Fassung der Klasse Widget
in Listing 7.11 deutlich einfacher und damit weniger fehleranfällig.
directInitializationOfClassElements.cpp
Aufgabe 7-6
Wenden Sie Listing 7.11 in einem kleinen Programm an.
Schreiben Sie ein kleines Programm rund um Listing 7.11 und seien Sie gespannt darauf, ob Ihr C++-Compiler das direkte Initialisieren von Klassenelementen in der erweiterten Form von C++11 unterstützt
Ermöglichen die neuen Schlüsselwörter default
und delete
, vom Compiler erzeugte Methoden anzufordern oder zu unterdrücken, so ermöglichen die neuen Schlüsselwörter override
und final
mehr Kontrolle über Ableitungshierarchien. Entsprechend dem expliziten Konvertierungskonstruktor erlaubt der explizite Konvertierungsoperator es nun in C++11, implizite Konvertierungen zu unterbinden.
An die Syntax zur Definition von rein virtuellen Funktionen angelehnt (virtual pureVirtual()=0
;), führt C++11 die zwei neuen Schlüsselwörter default
und delete
ein. Dabei erlaubt default
, die vom Compiler erzeugte Implementierung spezieller Methoden oder Operatoren zu nutzen. Hingegen ermöglicht delete
, die Definition automatisch sichtbarer Funktionen zu unterbinden.
Instrumentalisieren Sie den Compiler für Ihren Nutzen.
Von einer anderen Perspektive aus betrachtet, besitzt der Programmierer durch die zwei neuen Schlüsselwörter default
und delete
sehr mächtige Werkzeuge, um die automatische Erzeugung von Methoden, Operatoren und Funktionen explizit zu steuern. Er kann den Compiler für seinen Nutzen instrumentalisieren.
In Teil I, wurden einige typische Anwendungsfälle, wie default
und delete
, gezeigt, die bewirken, dass Objekte nicht kopierbar sind, den von Compiler erzeugten Standardkonstruktor besitzen und auf dem Heap angelegt werden. Was nun folgt, sind die Details.
Es verwundert immer wieder, wenn man sich vergegenwärtigt, welche Methoden und Operatoren der Compiler bei Bedarf erzeugt.
Methoden:
Standardkonstruktor
Kopierkonstruktor
Zuweisungsoperator
Destruktor
Operatoren:
operator new
operator delete
Adresse von
Indirektion
Elementzugriff
Elementindirektion
Operatoren (neu mit C++11):
Move-Konstruktor
Move-Zuweisungsoperator
Die Details lassen sich in Anhang D, nachlesen.
Regeln für automatisch erzeugte Methoden
Bezüglich der automatisch erzeugten Methoden sind noch ein paar Regeln im Gedächtnis zu behalten.
Die implizit erzeugten Methoden sind public
, inline
und nicht explicit
.
Sobald ein Konstruktor definiert wird, erzeugt der Compiler den Standardkonstruktor nicht mehr automatisch.
Ein definierter Move-Konstruktor unterdrückt den automatisch erzeugten Kopierkonstruktor.
Ein definierter Move-Zuweisungsoperator unterdrückt den automatisch erzeugten Copy-Zuweisungsoperator.
Die Aussagen zu Move-Konstruktor und Move-Zuweisungsoperator gelten auch in die andere Richtung.
Mächtigkeit von default
Diese mächtige Compiler-Funktionalität lässt sich nur explizit mit default
nutzen. So kann:
eine Methodenimplementierung in den Fällen erzwungen werden, in denen der Compiler diese nicht automatisch erzeugt.
der Programmierer die optimierte Implementierung der Compiler-Version verwenden.
der Anwender die Charakteristik der Methode auf private
, virtual
oder auch explicit
ändern, aber die Standardimplementierung des Compilers verwenden.
Nicht trivial
Die vom Compiler implizit erzeugte Methode oder der Operator ist trivial. Nicht trivial ist sie dann, wenn der Anwender die Charakteristik dieser speziellen Funktion verändert. Dies umfasst insbesondere die Punkte:
Zugriff
Virtualität
Expliziter Konstruktor
Ausnahmespezifizierung
const
-Eigenschaften der Parameter
In diesem Fall muss die Methode außerhalb des Klassenkörpers definiert werden.
Listing 7.12 soll den verwirrenden Sachverhalt auflösen.
class MyData{ public: explicit MyData(const MyData&); // 3 MyData& operator= (MyData&); // 5 virtual ~MyData() throw(); // 2, 4 private: MyData(); // 1 }; MyData::MyData()=default; // 1 MyData::~MyData() throw() = default; // 2, 4 MyData::MyData(const MyData&)= default; // 3 MyData& MyData::operator=(MyData&)= default; // 5
Listing 7.12 stellt ein paar nicht triviale spezielle Methoden und Operatoren vor. So ist der Default-Konstruktor (1) privat, der Destruktor ist virtuell (2), und er besitzt eine Ausnahmespezifikation (4), der Copy-Konstruktor ist explizit (3), und zuletzt nimmt der Zuweisungsoperator (5) sein Argument nicht const
an.
Trennung von Interface und Implementierung
Die Mächtigkeit von default
lässt sich in einem plakativen Satz zusammenfassen.
Listing 7.13 zeigt dieses einfache Prinzip explizit auf. In der Klasse MoveOnly
gibt der Entwickler die Deklaration des Default-Konstruktors in Zeile 6 vor, während der Compiler mit =default
für die Implementierung sorgt.
default.cpp
01 #include <utility> 02 class MoveOnly { 03 04 int data; 05 public: 06 MoveOnly()= default; 07 08 MoveOnly(const MoveOnly&) = delete; 09 10 MoveOnly(MoveOnly&& other): data(std::move(other.data)) {} 11 12 MoveOnly& operator=(const MoveOnly&) = delete; 13 14 MoveOnly& operator=(MoveOnly&& other) { 15 data=std::move(other.data); 16 return *this; 17 } 18 }; 19 20 int main(){ 21 22 // OK because of move-semantic 23 MoveOnly m1; 24 MoveOnly m2(std::move(m1)); 25 26 // ERROR because of copy-semantic 27 MoveOnly m3; 28 MoveOnly m4(m3); 29 30 }
MoveOnly
unterstützt nur die Move-Semantik, denn sowohl der Kopierkonstruktor (Zeile 8) als auch der Zuweisungsoperator (Zeile 12) ist auf delete
gesetzt. Interessant an der Implementierung des Move-Konstruktors (Zeile 10) und des Move-Zuweisungsoperators (Zeile 14) ist, dass sie auf die Methode std::move
zurückgreifen, um die Ressource explizit zu transferieren. Daher ist auch der Aufruf MoveOnly m2(std::move(m1))
in Zeile 24 gültig. Die Ausführung des Programms führt zur erwarteten Fehlermeldung, da der Kopierkonstruktor (Zeile 28) nicht unterstützt wird.
Eindeutiger kann eine Fehlermeldung nicht sein.
Mächtigkeit von delete
Ist es das Ziel, die vom Compiler erzeugte Funktion zu löschen, ist delete
das Mittel der Wahl in C++11. Die Funktionalität von delete
ist nicht auf die gleichen Methoden oder auch Operatoren wie default
beschränkt. Zwei typische Anwendungsfälle bieten sich für delete
an:
Die Definition von Funktionen löschen, die per Default vom Compiler erzeugt werden.
Die kritische Konvertierung von Datentypen verhindern.
Listing 7.14 soll diese beiden Anwendungsfälle verdeutlichen. Die Klasse TypeOnHeap
(Zeile 1) löscht den implizit erzeugten Destruktor. Damit können automatische Variablen nicht mehr angelegt werden, da die C++-Laufzeit den Destruktor beim automatischen Löschen des Objekts benötigt. Ähnlich interessant ist die Struktur OnlyInt
, die nur mit int
-Argumenten instanziiert werden kann. Erreicht wird dies durch die zwei Konstruktoren (Zeilen 7 und 10). Der erste wird ausschließlich für int
-Argumente, der zweite wird bei jedem anderen beliebigen Argument aufgerufen. Damit wird jede Konvertierung nach int
unterbunden, die ohne das Funktions-Template (Zeile 9) stattfinden würde.
delete.cpp
01 class TypeOnHeap{ 02 public: 03 ~TypeOnHeap()= delete; 04 }; 05 06 struct OnlyInt{ 07 OnlyInt(int){} 08 09 template<typename T> 10 OnlyInt(T) = delete; 11 12 }; 13 14 int main(){ 15 16 TypeOnHeap* toH= new TypeOnHeap; 17 OnlyInt onlyInt(5); 18 19 TypeOnHeap(); 20 static TypeOnHeap th; 21 22 OnlyInt(5L); 23 OnlyInt(5LL); 24 OnlyInt(5UL); 25 OnlyInt(5.5); 26 }
Dieses beeindruckende Ergebnis zeigt der Versuch, den Sourcecode zu übersetzen.
virtualDestruktor.cpp
Aufgabe 7-7
Verifizieren Sie, dass der vom Compiler erzeugte triviale virtuelle Destruktor performanter als der vom Anwender implementierte ist.
Polymorphe Basisklassen benötigen einen virtuellen Destruktor, denn das Löschen einer abgeleiteten Klasse durch einen Zeiger auf die polymorphe Basisklasse ist undefiniert, wenn dessen Destruktor nicht virtuell ist.
Das Unheil lässt sich schnell skizzieren:
class Base{ public: ~Base(){}; }; class Derived: public Base{}; ... Base* base= new Derived(); delete base;
C++11 bietet zwei Möglichkeiten an, den trivialen, virtuellen Destruktor zu definieren:
Die interessante Eigenschaft kommt zum Schluss. Der vom Compiler erzeugte virtuelle Destruktor soll performanter sein. Die Frage ist nun, wie kann das verifiziert werden?
Aufgabe 7-8
Implementieren Sie das Singleton Pattern mit den neuen Schlüsselwörtern default
und delete
.
Eines der bekanntesten Design Patterns ist das Singleton Pattern (Singleton, 2011). Dieses Muster sichert im klassischen Fall zu, dass es nur ein Objekt einer Klasse gibt. Erreicht wird dieses Verhalten der Singleton-Klasse dadurch, dass der Standardkonstruktor privat ist und sowohl der Copy-Konstruktor als auch der Zuweisungsoperator privat deklariert sind. Um die einzige Instanz der Singleton-Klasse zu erhalten, bietet diese die statische Methode getInstance
an. Beim ersten Aufruf der statischen Methode getInstance
wird die statische Instanz erzeugt. Diese Prosa lässt sich auch in C++-Code formulieren (Listing 7.16).
class MySingleton{ public: static MySingleton& getInstance(){ static MySingleton singleton; return singleton; } private: MySingleton() {} ~MySingleton() {} MySingleton(const MySingleton&); MySingleton& operator=(const MySingleton&); };
Schreiben Sie MySingleton
mit den neuen Sprachmitteln default
und delete
in C++11. Beachten Sie, welche der Methoden trivial bzw. nicht trivial sind. Verwenden Sie die Singleton-Klasse in einem minimalen Programm.
deleteVirtual.cpp
Aufgabe 7-9
Wenden Sie die virtuelle und die nicht virtuelle Ableitung in einer Klassenhierarchie an.
In Abbildung 7.4 ist die – zugegeben konstruierte – Ableitungshierarchie vorgegeben.
Implementieren Sie für jede Klasse eine Methode showMe
, die den Namen der Klasse ausgibt. Variieren Sie nun, indem Sie Methoden der Hierarchie als delete
deklarieren oder auch als virtuell erklären. Entspricht die Ausgabe des Programms Ihren Erwartungen?
Eine Klassenhierarchie, nicht nur von grafischen Frameworks, kann leicht unübersichtlich werden. Diese Problematik wird noch dadurch verstärkt, dass diese gern im Fluss sind. Nicht nur die Applikation, die das Framework nutzt, sondern das Framework selbst befindet sich häufig in der Überarbeitung. Um sicherzustellen, dass virtuelle Funktionen tatsächlich eine Methode der Basisklasse überschreiben und dass eine virtuelle Methode nicht irrtümlich überschrieben wird, führt C++11 zwei neue Bezeichner ein, override
und final
. Damit stellt der Compiler sicher, dass der Vertrag eingehalten wird. Tabelle 7.2 fasst die Eigenschaften der zwei Attribute zusammen.
Attribut | Eigenschaft |
| Die Methode muss eine virtuelle Methode der Klassenhierarchie überschreiben. |
| Die virtuelle Methode darf nicht in abgeleiteten Klassen überschrieben werden. |
Beide Schlüsselwörter werden erst durch den aktuellen GCC 4.7 (GCC 4.7, 2011) unterstützt.
virtualFunctionsOverride.cpp
01 class Base { 02 03 void func1(); 04 virtual void func2(float); 05 virtual void func3() const; 06 virtual long func4(int); 07 08 virtual void f(); 09 virtual void h(int) final; 10 }; 11 12 class Derived: public Base { 13 14 // ill-formed; no virtual method func1 exists 15 virtual void fun1() override; 16 17 // ill-formed: bad type 18 virtual void func2(double) override; 19 20 // ill-formed: const missing 21 virtual void func3() override; 22 23 // ill-formed: wrong return type 24 virtual int func4(int) override; 25 26 // well-formed: f override Base::f 27 virtual void f() override; 28 29 // well-formed: a new (final) virtual method 30 virtual void g(long) final; 31 32 // ill-formed: base method declared final 33 virtual void h(int); 34 35 // well-formed: a new virtual function 36 virtual void h(double); 37 38 }; 39 40 int main(){ 41 42 Base base; 43 Derived derived; 44 45 }
Ein genaueres Studium von Listing 7.17 zeigt, dass das beabsichtigte, aber falsche Erklären neuer virtueller Methoden oder auch das unbeabsichtigte Überschreiben von Methoden der Klassenhierarchie, die als final
deklariert wurden, deutlich schwieriger ist. Denn der Compiler prüft nicht nur den Namen, sondern auch die Parameter (Zeile 18), den Rückgabetyp (Zeile 24) und die const
Zusicherung der Methode (Zeile 21). Genau dies moniert der GCC 4.7.
final class
Die Geschichte mit final
ist noch nicht zu Ende. Durch den Identifier final
kann eine Klasse als final
ausgezeichnet werden. Dadurch ist es nicht mehr möglich, von ihr abzuleiten.
Das Übersetzen des kleinen Codeschnipsels in Listing 7.18 quittiert der Compiler mit einer eindeutigen Fehlermeldung, zu sehen in Abbildung 7.6.
Aufgabe 7-10
Verwenden Sie für die Implementierung der Template-Methode die Identifier final
und override
.
Die Schablonenmethode (template method) gibt eine Ablaufstruktur für eine Familie von Algorithmen vor, die aus mehreren Einzelschritten besteht. Während die Reihenfolge der Einzelschritte feststeht, hängt die Logik der Einzelschritte von dem konkreten Algorithmus ab. Die Details zur Schablonenmethode lassen sich bei Wikipedia (Schablonenmethode, 2011) nachlesen.
Entwerfen Sie eine abstrakte Basisklasse Sort
, die eine Methode processData
besitzt. Es soll nicht möglich sein, die Methode zu überschreiben. Diese Methode soll die virtuellen Methoden readData
, sortData
und writeData
in dieser Reihenfolge aufrufen. Während die Basisklasse Sort
einfache Implementierungen für readData
und writeData
vorhält, soll die Methode sortData
rein virtuell sein. Implementieren und instanziieren Sie eine Dummyklasse QuickSort
, um Ihren Entwurf zu testen. Implementieren Sie dazu auch die zwei Methoden readData
und writeData
. Stellen Sie sicher, dass die Methoden die Methoden der Basisklasse Sort
überschreiben.
Konvertierungskonstruktor und Konvertierungsoperator
Beim Entwurf der Klasse MyClass
stehen dem C++-Entwickler zwei Wege offen, die Konvertierung seines Datentyps MyClass
in einen fremden Datentyp anzubieten. Über einen Konvertierungskonstruktor wird der fremde Datentyp zu MyClass
konvertiert. Über einen Konvertierungsoperator wird MyClass
in einen fremden Datentyp konvertiert. Abbildung 7.7 stellt diese beiden Richtungen exemplarisch dar.
Listing 7.19 zeigt die zwei Richtungen in Anwendung.
convertImplicit.cpp
01 class A{}; 02 03 class B{}; 04 05 class MyClass{ 06 public: 07 MyClass(){} 08 MyClass(A){} 09 operator B(){ return B(); } 10 }; 11 12 void needMyClass(MyClass){}; 13 void needB(B){}; 14 15 int main(){ 16 17 // A -> MyClass 18 A a; 19 20 // explicit invocation 21 MyClass myClass1(a); 22 // implicit conversion from A to MyClass 23 MyClass myClass2= a; 24 needMyClass(a); 25 26 // MyClass -> B 27 MyClass myCl; 28 29 // explicit invocation 30 B b1(myCl); 31 // implicit conversion from MyClass to B 32 B b2= myCl; 33 needB(myCl); 34 35 }
Da die heimliche Konvertierung nicht immer erwünscht ist, lässt sich durch die Angabe des Schlüsselworts explicit
die implizite Konvertierung nach MyClass
in den Zeilen 23 und 24 unterbinden. Leider ist die Angabe des Schlüsselworts explicit
im C++98-Standard nur für den Konvertierungskonstruktor möglich, aber nicht für den Konvertierungsoperator. Damit lassen sich die Aufrufe in den Zeilen 32 und 33 nicht verhindern.
Mit C++11 wurde diese Asymmetrie beseitigt. Sowohl der Konvertierungskonstruktor als auch der Konvertierungsoperator von MyClass
ist explicit
deklariert (Listing 7.20).
convertExplicit.cpp
01 class A{}; 02 03 class B{}; 04 05 class MyClass{ 06 public: 07 MyClass(){} 08 explicit MyClass(A){} // since C++98 09 explicit operator B(){} // new with C++11 10 }; 11 12 void needMyClass(MyClass){}; 13 void needB(B){}; 14 15 int main(){ 16 17 // A -> MyClass 18 A a; 19 20 // explicit invocation 21 MyClass myClass1(a); 22 // implicit conversion from A to MyClass 23 MyClass myClass2= a; 24 needMyClass(a); 25 26 // MyClass -> B 27 MyClass myCl; 28 29 // explicit invocation 30 B b1(myCl); 31 // implicit conversion from MyClass to B 32 B b2= myCl; 33 needB(myCl); 34 35 }
Die entscheidenden Zeilen in Listing 7.20 sind die Schlüsselwörter explicit
in den Zeilen 8 und 9. Sie führen dazu, dass das Programm nicht mehr kompiliert.
bool
Die Konvertierung nach bool
mit dem expliziten Konvertierungsoperator verhält sich speziell. So ignoriert der Compiler das Schlüsselwort explicit
, damit der Datentyp in Ausdrücken verwendet werden kann, die zu den Wahrheitswerten true
oder false
evaluiert werden. Eine implizite Konvertierung nach int
führt der C++-Compiler hingegen nicht durch (Listing 7.21).
01 struct MyBool{ 02 explicit operator bool(){return true;} 03 }; 04 05 int main(){ 06 07 MyBool myB; 08 09 if (myB){}; 10 int a= (myB)? 3: 4; 11 int b= myB + a; 12 13 }
explicit operator bool()
in Zeile 2 bewirkt, dass der arithmetische Ausdruck in Zeile 11 nicht gültig ist. Der Bezeichner explicit
besitzt keinen Einfluss auf die implizite Konvertierung in den Zeilen 9 und 10 nach bool
.
Aufgabe 7-11
Überprüfen Sie Ihren Sourcecode auf implizite Konvertierungen.
Durchsuchen Sie Ihren Sourcecode nach der Definition nicht expliziter Konvertierungskonstruktoren und Konvertierungsoperatoren.
Entscheiden Sie für jede der impliziten Konvertierungsfunktionen, ob diese Eigenschaft beabsichtigt ist.
Falls die implizite Konvertierung nicht unterstützt werden soll, verwenden Sie das Schlüsselwort explicit
.
Übersetzen Sie das Programm, sodass der Compiler implizite Konvertierungen im Sourcecode entdeckt.
Wie erreichen Sie, dass der Code in Listing 2.22 gültig ist, obwohl Sie den Konvertierungsoperator nach bool
als explicit
erklärt haben.