namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


kennen:spezielle_methoden

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(const Empty& rhs) {}                             // Kopierkonstruktor
  Empty(Empty&& rhs)      {}                             // Verschiebekonstruktor
  Empty& operator=(const Empty& 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 deklariertnutzerdefiniert

(!) … 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(const Matrix& orig);
  Matrix& operator=(const Matrix& 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(const Matrix& 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= (const Matrix& 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, const Matrix& 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.

kennen/spezielle_methoden.txt · Zuletzt geändert: 2018-02-22 11:46 von rrichter