Dreierregel

aus Wikipedia, der freien Enzyklopädie
Wechseln zu: Navigation, Suche

Die Dreierregel bezeichnet in der Programmiersprache C++ eine Faustregel, die empfiehlt, dass in einer Klasse, die eine der folgenden drei Elementfunktionen definiert, auch die jeweils anderen beiden definiert werden sollten:[1]

  1. Kopierkonstruktor
  2. Destruktor
  3. Zuweisungsoperator

Hintergrund[Bearbeiten | Quelltext bearbeiten]

Diese drei 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 (1 und 3) bzw. in umgekehrter Reihenfolge freigegeben/abgeräumt (2).

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

Beispiele[Bearbeiten | Quelltext bearbeiten]

Ressourcen über Handles[Bearbeiten | Quelltext 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 | Quelltext 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.

Ausnahmen von der Dreierregel[Bearbeiten | Quelltext bearbeiten]

Die Dreierregel ist keine Regel, die es ausnahmslos zu befolgen gilt, sondern eher eine Faustregel oder Empfehlung. Ein konkretes Beispiel, bei welchem die Dreierregel nicht angewendet werden muss, ist ein Referenzzähler: Beim Erzeugen (Standardkonstruktor), Kopieren (Kopierkonstruktor) und Zerstören (Destruktor) ändert sich die Anzahl der Objekte, nicht jedoch beim Zuweisen:

struct RefCounter
{
	static std::size_t n;

	RefCounter() // Standardkonstruktor
	{
		++n;
	}
	RefCounter(RefCounter const&) // Kopierkonstruktor
	{
		++n;
	}
	~RefCounter() // Destruktor
	{
		--n;
	}

	// Kein Zuweisungsoperator benötigt
	// RefCounter& operator= (RefCounter const&)
};

Seit C++11[Bearbeiten | Quelltext bearbeiten]

Seit dem Erscheinen von C++11 wird diese Regel zur Fünferregel, in der auch

definiert werden sollten.[2][3]

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.[4]

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, das 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 | Quelltext bearbeiten]

  • Stanley B. Lippman, Josèe Lajoie, Barbara E. Moo: C++ Primer. 4. Auflage. Addison-Wesley Professional, 2005, ISBN 0-201-72148-1.

Einzelnachweise[Bearbeiten | Quelltext bearbeiten]

  1. Bjarne Stroustrup: The C++ Programming Language. 3. Auflage. Addison-Wesley, 2000, ISBN 0-201-70073-5, S. 283–284.
  2. Proposing the Rule of Five (PDF)
  3. Proposing the Rule of Five, v2 (PDF)
  4. en.cppreference.com