Die Dreierregel (vor C++11) bzw. Fünferregel (ab C++11) (englisch Rule of three bzw. Rule of five) bezeichnet in der Programmiersprache C++ eine Empfehlung, die besagt, dass in einer Klasse, die eine der folgenden drei bzw. fünf Elementfunktionen definiert, auch die jeweils anderen beiden bzw. vier definiert werden sollten:[1]

  1. Destruktor
  2. Copy-Konstruktor
  3. Copy-Zuweisungsoperator
  4. Move-Konstruktor
  5. Move-Zuweisungsoperator

Die Regel ist verpflichtender Bestandteil gängiger Konventionen wie z. B. C++ Core Guidelines[2] oder AUTOSAR[3].

Hintergrund

Bearbeiten

Die oben genannten Elementfunktionen werden normalerweise vom Compiler automatisch generiert. Diese generierten Versionen haben dabei eine in der Sprachnorm festgelegte Bedeutung: Es werden alle nicht-statischen Datenelemente in der Reihenfolge ihrer Deklaration kopiert (2 und 3) bzw. verschoben (4 und 5) bzw. in umgekehrter Reihenfolge freigegeben/abgeräumt (1).

Falls eine Klasse jedoch eine andere Semantik hat, z. B. weil sie eine Ressource als Datenelement enthält, die nicht auf diese Weise kopiert, verschoben oder abgeräumt werden kann, kann jede dieser Elementfunktionen durch eine eigene Definition ersetzt werden. In den meisten Fällen erfordern solche Klassen dann, dass alle drei bzw. fünf dieser Elementfunktionen eine eigene, benutzerdefinierte Implementierung benötigen.

Beispiele

Bearbeiten

Ressourcen über Handles

Bearbeiten

Repräsentiert ein Datenelement eine Ressource (z. B. eine Datei, TCP- oder Datenbankverbindung) über ein Handle, so ist dem Compiler diese Bedeutung i. d. R. nicht bekannt. Das Datenelement ist z. B. vom Typ int. Damit im Destruktor der Klasse die repräsentierte Ressource geschlossen bzw. freigegeben wird, muss die Klasse einen benutzerdefinierten Destruktor haben, in dem die Ressource über einen Systemaufruf explizit geschlossen/freigegeben wird.

Ebenso müssen Kopierkonstruktor und Zuweisungsoperator mehr machen, als nur das Handle zu kopieren, damit Originalobjekt und Kopie konfliktfrei auf die Ressource zugreifen können. Falls auf die Ressource nicht von mehreren Objekten aus zugegriffen werden kann oder soll, sind die beiden Elementfunktionen explizit zu löschen, was bewirkt, dass ein Objekt dieses Typs nicht kopiert werden kann:

class Datei
{
public:
    Datei(const char* dateiname)
    : file(fopen(dateiname, "rb"))
    { /* Fehlerbehandlung usw. */ }

    // Dreierregel:
    Datei(const Datei&) = delete; // Kein Kopieren!
    ~Datei() { fclose(file); }
    void operator=(const Datei&) = delete; // Kein Kopieren!

    // weitere Elementfunktionen
    // ...

private:
    FILE* file;
};

Ressource über „nackte“ Zeiger

Bearbeiten

Ähnlich ist es, wenn eine Ressource über einen „nackten Zeiger“ referenziert wird. Die compilergenerierten Funktionen kopieren zwar den Zeiger, aber nicht die darüber referenzierte Ressource. Dies wird auch flache Kopie genannt. Dadurch teilen sich Ursprungsobjekt und Kopie die Ressource. Außerdem ist dabei nicht klar, welchem der Objekte die geteilte Ressource „gehört“, also wer für das Abräumen der Ressource zuständig ist.

Also benötigt die Klasse benutzerdefinierte Definitionen für den Kopierkonstruktor und den Zuweisungsoperator. In diesen muss dann entweder die referenzierte Ressource explizit dupliziert werden (tiefe Kopie) oder der Zugriff auf andere Weise geregelt werden, gegebenenfalls ist auch hier die einzig sinnvolle Lösung, das Kopieren von Objekten dieser Klasse durch explizites Löschen dieser Elementfunktionen ganz zu verbieten.

Moderne Compiler bieten die Möglichkeit, eine Warnung auszugeben, wenn eine Klasse definiert wird, die „nackte Zeiger“ als Datenelemente enthält, aber die Dreierregel nicht erfüllt.

Eine andere Möglichkeit ist die Verwendung von Smart Pointern, welche die referenzierte Ressource auf eine definierte Weise kapseln und dabei auch den Zugriff und die Lebensdauer einer möglicherweise geteilten Ressource klar regeln. Die C++-Standardbibliothek stellt seit C++11 dafür eigene Smart-Pointer-Klassen zur Verfügung:

Smart Pointer Klasse Bedeutung
std::unique_ptr<T> Ressource kann nicht implizit kopiert werden. Der Compiler gibt einen Fehler aus, wenn versucht wird, ein Objekt einer Klasse zu kopieren, die unique_ptr-Datenelemente, aber keine benutzerdefinierten Kopierfunktionen hat.
std::shared_ptr<T> Die referenzierte Ressource wird vom Original- und Ziel-Objekt geteilt genutzt. Über einen Zähler wird die Anzahl der Kopien vermerkt, so dass die Ressource abgeräumt/freigegeben werden kann, wenn die Lebensdauer der letzten Kopie endet.
std::weak_ptr<T>

unique_ptr und shared_ptr enthalten zudem eine optionale "Deleter"-Funktion, falls die referenzierte Ressource nicht einfach über delete freigegeben werden kann.

Seit C++11

Bearbeiten

Seit dem Erscheinen von C++11 wurde die Dreierregel mit den beiden Funktionen

zur Fünferregel (Rule of five) erweitert.[4][5]

Andererseits sollten die Verantwortlichkeiten der Klassen getrennt werden:

  1. Klassen, die jeweils genau eine Ressource halten. Für diese gelten dann im Allgemeinen die Dreier- oder Fünferregel, aber man braucht nur wenige dieser Ressourcenverwaltungsklassen. Auch können oftmals die bestehenden Smart-Pointer der Standardbibliothek dafür verwendet werden.
  2. Klassen, die in ihren Datenmembern lediglich andere Ressourcen aggregieren. Diese Klassen brauchen dann keine benutzerdefinierten Kopierfunktionen oder Destruktoren, da die compilergenerierten Funktionen dann automatisch die korrekte Semantik beinhalten. Das vereinfacht das Implementieren und Testen dieser Klassen erheblich.

Diese Herangehensweise wird auch Rule of zero genannt, da die überwiegende Mehrheit der Klassen zur zweiten Kategorie ohne benutzerdefinierte Kopierfunktionen und Destruktoren gehören.[6]

Seit C++11 ist es zudem möglich, das Erzeugen der compilergenerierten Version nicht nur explizit zu unterdrücken, sondern auch explizit zu erzwingen (=default). Damit wird dem Compiler (und auch dem menschlichen Leser) mitgeteilt, dass in diesem Fall die compilergenerierte Version genau das gewünschte Verhalten bietet, so dass man es nicht manuell implementieren muss:

class Example
{
    Example(const Example&) = default;  // erzwinge compilergenerierte Version
    void operator=(const Example&) = delete; // verhindere compilergenerierte Version
};

Literatur

Bearbeiten
  • Stanley B. Lippman, Josèe Lajoie, Barbara E. Moo: C++ Primer. 5. Auflage. Addison-Wesley Professional, 2012, ISBN 978-0-13-305303-6.

Einzelnachweise

Bearbeiten
  1. Bjarne Stroustrup: The C++ Programming Language. 3. Auflage. Addison-Wesley, 2000, ISBN 0-201-70073-5, S. 283–284.
  2. C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all, C++ Core Guidelines, 3. August 2020, abgerufen am 8. September 2020.
  3. Rule A12-0-1, Kapitel 6.12.0, Guidelines for the use of the C++14 language in critical and safety-related systems, AUTOSAR AP Release 2019-03, S. 201.
  4. Proposing the Rule of Five (PDF)
  5. Proposing the Rule of Five, v2 (PDF; 107 kB)
  6. The rule of three/five/zero, cppreference.com, abgerufen am 8. September 2020.