Namensräume
Varianten
Aktionen

Die Regel von drei/fünf/null

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 

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.

C.21: Wenn Sie eine Kopie-, Verschiebe- oder Destruktorfunktion definieren oder =delete, definieren oder =delete Sie alle.

[bearbeiten] Externe Links

  1. "Rule of Zero", R. Martinho Fernandes 08/15/2012
  2. "A Concern about the Rule of Zero", Scott Meyers, 3/13/2014.