Inhaltsverzeichnis
Spezielle Methoden
Learn the rules like a pro, so you can break them like an artist.— Pablo Picasso
Vom Compiler erzeugt
Schon eine leere Klasse oder Struktur
struct Empty {};
besitzt implizit generierte Methoden zum Erzeugen, Kopieren, Zuweisen, Verschieben und Vernichten von Objekten:
struct Empty { Empty() {} // Standardkonstruktor Empty(Empty const& rhs) {} // Kopierkonstruktor Empty(Empty&& rhs) {} // Verschiebekonstruktor Empty& operator=(Empty const& rhs) { return *this; } // Kopierzuweisung Empty& operator=(Empty&& rhs) { return *this; } // Verschiebezuweisung ~Empty() {} // Destruktor };
In vielen Klassen ist dieses Standardverhalten ausreichend:
struct Punkt { int x, y; }; void f() { Punkt a; // Standardkonstruktor Punkt b { 1, 2 }; // POD-Konstruktor Punkt c (a); // Kopierkonstruktor a = b; // Zuweisung std::swap(a, b); // Tausch nutzt Verschiebekonstruktor und -zuweisung } // Destruktor
Die Elemente der Klasse werden erzeugt, kopiert, verschoben und vernichtet, indem deren spezielle Methoden aufgerufen werden. Einige Regeln sind leicht einzusehen.
- Das "Verschieben" von Grunddatentypen (z.B.
int
) erfolgt durch Kopieren des Wertes. - Ein Objekt lässt sich nur dann kopieren bzw. verschieben, wenn die geforderte Aktion für alle Elemente des Objekts ausführbar ist.
- Einem konstanten Attribut lässt sich kein neuer Wert zuweisen.
- Verbietet man das Vernichten von Objekten, wäre schon deren Erzeugung ein Fehler.
Andere Regeln sind einzuhalten, um Besitzverhalten und Verwaltung und Freigabe von Ressourcen beim Kopieren und Verschieben festzulegen.
default und delete
Die speziellen Methoden können
ab C++11 durch Markieren als = default
anstelle eines Funktionsrumpfes mit dem Standardverhalten realisiert
oder durch die Angabe = delete
ausdrücklich verboten werden.
(siehe Beispiel default, delete).
Vor C++11 konnten Kopie bzw. Zuweisung unmöglich gemacht werden,
indem diese Methoden als private
gekennzeichnet, aber nicht implementiert wurden.
Regeln
Die speziellen Methoden können vom Nutzer umdefiniert werden. Dann werden einige der anderen Methoden nicht mehr automatisch erzeugt:
Nutzer definiert | |||||||
---|---|---|---|---|---|---|---|
nichts | Konstruktor | Destruktor | Kopierkonstruktor | Zuweisungsoperator | Verschiebekonstruktor | Verschiebezuweisung | |
Standardkonstruktor | = default | nicht deklariert | = default | nicht deklariert | = default | nicht deklariert | = default |
Destruktor | = default | = default | nutzerdefiniert | = default (!) | = default (!) | = default | = default |
Kopierkonstruktor | = default | = default | = default (!) | nutzerdefiniert | = default (!) | = delete | = delete |
Zuweisungsoperator | = default | = default | = default (!) | = default (!) | nutzerdefiniert | = delete | = delete |
Verschiebekonstruktor | = default | = default | nicht deklariert | nicht deklariert | nicht deklariert | nutzerdefiniert | nicht deklariert |
Verschiebezuweisung | = default | = default | nicht deklariert | nicht deklariert | nicht deklariert | nicht deklariert | nutzerdefiniert |
(!) … sollte vom Nutzer verboten oder umdefiniert werden
Besitzt eine Klasse einen nichttrivialen Destruktor (Ressourcenfreigabe), sind zumeist auch Kopierkonstruktor und Zuweisungsoperator in geeigneter Weise zu implementieren (Rule of Three). Die in C++11 eingeführte Verschiebekonstruktor und Verschiebezuweisung wird in diesem Fall nicht automatisch erzeugt. Deren Definition durch den Nutzer wiederum unterdrückt das automatische Erzeugen der anderen speziellen Funktionen. Eigene Versionen davon können das Laufzeitverhalten der Klasse durch das Vermeiden teurer Kopien verbessern (Rule of Five).
Besitzverhalten
Die Verwaltung von Ressourcen (dynamischer Speicher, Sperren auf Dateien, Mutexe) wird durch C++ erleichtert, wenn beim Erschaffen der Ressource ein Objekt angelegt wird, welches bei seiner Vernichtung im Destruktor diese Ressource automatisch freigibt (RAII-Prinzip). Der Besitzer ist für die Freigabe verantwortlich. Bjarne Stroustrup bezeichnet dies als die beste Form von garbage collection.
Eine Klasse für n-reihige, quadratische Matrizen soll als Beispiel dienen.
class Matrix { public: explicit Matrix(int n); : n(n) , data(new double[n*n]) { std::fill(data, data + n*n, 0.0); } Matrix(); ~Matrix(); Matrix(Matrix const& orig); Matrix& operator=(Matrix const& rhs); Matrix(Matrix&& source); Matrix& operator=(Matrix&& rhs); // Elementzugriff, operator+=, ... private: int n; double* data; };
Je nach Zeilenzahl n wird der Speicher für die Zahlen dynamisch angelegt. Neben den Rechenoperationen sind die speziellen Methoden zu definieren, um die Besitzverhältnisse zu regeln.
Destruktor
Die Freigabe des Speichers erfolgt am Ende der Lebensdauer der Matrix:
Matrix::~Matrix() { delete[] data; }
Standardkonstruktor
Eine leere Matrix hat keinen Speicher. Dennoch muss der Zeiger sinnnvoll gesetzt werden.
Matrix::Matrix() : n(0) , data(nullptr) { }
Kopierkonstruktor
Ein Kopierkonstruktor erzeugt eine Kopie von einem Objekt gleichen Typs. Im Standardverhalten werden die klassen#Attribute elementweise kopiert: Die (flache) Kopie eines Zeigers zeigt auf den gleichen Speicherplatz. Dieser würde durch die Destruktoren des Originals und der Kopie zweimalig freigegeben — mit fatalen Folgen. Ein nicht-trivialer Destruktor zum Freigeben von Ressourcen ist ein verlässlicher Hinweis darauf, dass Kopierkonstruktor und Zuweisungsoperator definiert oder verboten werden sollten.
Für die Matrix wird eine tiefe Kopie erzeugt:
Matrix::Matrix(Matrix const& orig) : n(orig.n) , data(new double[orig.n*orig.n]) { std::copy(orig.data, orig.data + n*n, data); }
Andere Herangehensweisen wären gemeinsamer Besitz (shared resource) oder lazy copying (copy on write).
Zuweisungsoperator
Die kanonische Implementierung der Kopierzuweisung prüft, ob überhaupt zugewiesen werden muss, legt dann ein Kopie des rechten Operanden (rhs = right-hand side) an und vertauscht dann die Zeiger auf die Inhalte mit der Kopie. Tritt beim Anlegen der Kopie eine Ausnahme auf, bleibt die Matrix unverändert.
Matrix& Matrix::operator= (Matrix const& rhs) { if (this != &rhs) { Matrix tmp(rhs); std::swap(tmp.data, data); std::swap(tmp.n, n); } return *this; }
Verschiebekonstruktor
Beim Verschieben geht der Besitz von Ressourcen auf das neue Objekt über.
Der Verschiebekonstruktor (engl. move constructor)
übernimmt dazu die Elemente eines als Rvalue-Referenz
übernommenen Objekts.
Als nun leere Hülle bekommt die Quelle im Austausch
einen nullptr
zum Vernichten:
Matrix::Matrix(Matrix&& source) : n(0) , data(nullptr) { std::swap(source.data, data); std::swap(source.n, n); }
Verschiebezuweisung
Die Verschiebezuweisung (engl. move assignment) ist dann in ähnlicher Weise zu definieren:
Matrix& Matrix::operator= (Matrix&& source) { std::swap(source.data, data); std::swap(source.n, n); return *this; }
Kopier- und Verschiebezuweisung können in einer Methode zusammengefasst werden (copy-and-swap-Idiom, auch "Rule of Four and a half" genannt):
Matrix& Matrix::operator= (Matrix rhs) { std::swap(rhs.data, data); std::swap(rhs.n, n); return *this; }
Hier ist rhs
schon eine Kopie, falls notwendig. Zwischenergebnisse werden per Verschiebekonstruktor übernommen oder die Kopie ganz vermieden (copy elision).
Verschiebesemantik bringt Zeitgewinn
Werden nun noch die fehlenden Rechenoperationen
definiert (Verschiebekonstruktor für a
, falls Zwischenergebnis,
und Verschiebung des Rückgabewertes)
Matrix operator+(Matrix a, Matrix const& b) { a += b; return a; } void demo() { Matrix a(3000), b(3000); a(0,0) = a(1,1) = a(2,2) = 1; b(2,0) = b(1,1) = b(0,2) = 1; Matrix c = a + b + a; c = b + a + b; // ... }
erfordern die Anweisungen zur Berechnung der Matrix c
durch die Verschiebesemantik nur noch zwei statt sechs teure Kopien
für die ersten Zwischenergebnisse.
Von Containern bereitgestellt
Das Arbeit mit "nackten", besitzverwaltenden Zeigern
bringt viel Verantwortung mit sich und kann im Ausnahmefall(!) schiefgehen.
Container wie std::vector<T>
implementieren
das Kopier- und Verschiebeverhalten von vornherein und
kapseln die Zeiger auf den dynamischen Speicher.
Nutzt man diese schon vorhandenen Klassen zur Datenhaltung in der eigenen Klasse,
braucht man sich um Kopie, Zuweisung und Verschiebesemantik nicht selbst zu kümmern.
So reduziert sich der zu schreibende Quellcode auf wenige, wesentliche Zeilen
(Rule of Zero):
class Matrix { public: explicit Matrix(int n) : n(n) , data(n*n, 0.0) { } Matrix() : n(0) {} // Elementzugriff, operator+=, ... private: int n; std::vector<double> data; };
Besitzverwaltende Zeiger auf Einzelobjekte lassen in referenzzählenden std::shared_ptr<T> und nur verschiebbaren std::unique_ptr<T> verbergen.