Die Regel von drei/fünf/null
Inhalt |
[bearbeiten] Regel von drei
Wenn eine Klasse einen benutzerdefinierten Destruktor, einen benutzerdefinierten Kopierkonstruktor oder einen benutzerdefinierten Kopierzuweisungsoperator erfordert, erfordert sie mit ziemlicher Sicherheit alle drei.
Da C++ Objekte von benutzerdefinierten Typen in verschiedenen Situationen (Übergabe/Rückgabe per Wert, Manipulation eines Containers usw.) kopiert und kopierzuweist, werden diese speziellen Member-Funktionen aufgerufen, wenn sie zugänglich sind, und wenn sie nicht benutzerdefiniert sind, werden sie vom Compiler implizit definiert.
Die implizit definierten speziellen Member-Funktionen sollten nicht verwendet werden, wenn die Klasse eine Ressource verwaltet, deren Handle ein Objekt eines Nicht-Klassentyps ist (Rohzeiger, POSIX-Dateideskriptor usw.), dessen Destruktor nichts tut und dessen Kopierkonstruktor/Zuweisungsoperator eine "flache Kopie" durchführt (kopiert den Wert des Handles, ohne die zugrundeliegende Ressource zu duplizieren).
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: explicit rule_of_three(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // allocate std::strcpy(cstring, s); // populate } } ~rule_of_three() // I. destructor { delete[] cstring; // deallocate } rule_of_three(const rule_of_three& other) // II. copy constructor : rule_of_three(other.cstring) {} rule_of_three& operator=(const rule_of_three& other) // III. copy assignment { // implemented through copy-and-swap for brevity // note that this prevents potential storage reuse rule_of_three temp(other); std::swap(cstring, temp.cstring); return *this; } const char* c_str() const // accessor { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. uses copy constructor std::cout << o2.c_str() << ' '; rule_of_three o3("def"); std::cout << o3.c_str() << ' '; o3 = o2; // III. uses copy assignment std::cout << o3.c_str() << '\n'; } // I. all destructors are called here
Ausgabe
abc abc def abc
Klassen, die nicht kopierbare Ressourcen über kopierbare Handles verwalten, müssen möglicherweise Kopierzuweisung und Kopierkonstruktor als private deklarieren und ihre Definitionen nicht bereitstellen(bis C++11)Kopierzuweisung und Kopierkonstruktor als = delete definieren(seit C++11). Dies ist eine weitere Anwendung der Regel von drei: das Löschen eines und das Belassen des anderen für die implizite Definition ist typischerweise falsch.
[bearbeiten] Regel von fünf
Da die Anwesenheit eines benutzerdefinierten (einschließlich mit = default oder = delete deklarierten) Destruktors, Kopierkonstruktors oder Kopierzuweisungsoperators die implizite Definition des Verschiebekonstruktors und des Verschiebezweisuungsoperators verhindert, muss jede Klasse, für die Verschiebesemantik wünschenswert ist, alle fünf speziellen Member-Funktionen deklarieren.
class rule_of_five { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: explicit rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // allocate std::strcpy(cstring, s); // populate } } ~rule_of_five() { delete[] cstring; // deallocate } rule_of_five(const rule_of_five& other) // copy constructor : rule_of_five(other.cstring) {} rule_of_five(rule_of_five&& other) noexcept // move constructor : cstring(std::exchange(other.cstring, nullptr)) {} rule_of_five& operator=(const rule_of_five& other) // copy assignment { // implemented as move-assignment from a temporary copy for brevity // note that this prevents potential storage reuse return *this = rule_of_five(other); } rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment { std::swap(cstring, other.cstring); return *this; } // alternatively, replace both assignment operators with copy-and-swap // implementation, which also fails to reuse storage in copy-assignment. // rule_of_five& operator=(rule_of_five other) noexcept // { // std::swap(cstring, other.cstring); // return *this; // } };
Im Gegensatz zur Regel von drei ist das Versäumnis, einen Verschiebekonstruktor und eine Verschiebezweisuung bereitzustellen, normalerweise kein Fehler, sondern eine verpasste Optimierungsmöglichkeit.
[bearbeiten] Regel von null
Klassen, die benutzerdefinierte Destruktoren, Kopier-/Verschiebekonstruktoren oder Kopier-/Verschiebezweisuungsoperatoren haben, sollten sich ausschließlich mit dem Besitz befassen (was sich aus dem Single Responsibility Principle ergibt). Andere Klassen sollten keine benutzerdefinierten Destruktoren, Kopier-/Verschiebekonstruktoren oder Kopier-/Verschiebezweisuungsoperatoren haben[1].
Diese Regel erscheint auch in den C++ Core Guidelines als C.20: Wenn Sie die Definition von Standardoperationen vermeiden können, tun Sie es.
class rule_of_zero { std::string cppstring; public: rule_of_zero(const std::string& arg) : cppstring(arg) {} };
Wenn eine Basisklasse für polymorphen Gebrauch vorgesehen ist, muss ihr Destruktor möglicherweise als public und virtual deklariert werden. Dies blockiert implizite Verschiebungen (und veraltet implizite Kopien), sodass die speziellen Member-Funktionen als = default definiert werden müssen[2].
class base_of_five_defaults { public: base_of_five_defaults(const base_of_five_defaults&) = default; base_of_five_defaults(base_of_five_defaults&&) = default; base_of_five_defaults& operator=(const base_of_five_defaults&) = default; base_of_five_defaults& operator=(base_of_five_defaults&&) = default; virtual ~base_of_five_defaults() = default; };
Dies macht die Klasse jedoch anfällig für Slicing, weshalb polymorphe Klassen Kopien oft als = delete definieren (siehe C.67: Eine polymorphe Klasse sollte öffentliche Kopie/Verschiebung unterdrücken in den C++ Core Guidelines), was zu folgender allgemeiner Formulierung für die Regel von Fünf führt.