Namespaces
Variants

The rule of three/five/zero

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Inhaltsverzeichnis

Regel von drei

Wenn eine Klasse einen benutzerdefinierten Destruktor , einen benutzerdefinierten Copy-Konstruktor , oder einen benutzerdefinierten Copy-Zuweisungsoperator benötigt, dann benötigt sie mit großer Wahrscheinlichkeit alle drei.

Da C++ Objekte benutzerdefinierter Typen in verschiedenen Situationen kopiert und kopierzuweist (Übergabe/Rückgabe als Wert, Manipulation eines Containers, etc.), werden diese speziellen Memberfunktionen aufgerufen, falls zugänglich, und falls sie nicht benutzerdefiniert sind, werden sie implizit vom Compiler definiert.

Die implizit definierten speziellen Elementfunktionen sollten nicht verwendet werden, wenn die Klasse eine Ressource verwaltet , deren Handle ein Objekt eines Nicht-Klassen-Typs ist (Raw-Pointer, POSIX-Dateideskriptor, etc), deren Destruktor nichts tut und der Kopierkonstruktor/Zuweisungsoperator ein "shallow copy" 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; // roher Zeiger, verwendet als Handle für einen
                   // dynamisch allokierten Speicherblock
public:
    explicit rule_of_three(const char* s = "") : cstring(nullptr)
    {   
        if (s)
        {   
            cstring = new char[std::strlen(s) + 1]; // allokieren
            std::strcpy(cstring, s); // befüllen
        }
    }
    ~rule_of_three() // I. Destruktor
    {
        delete[] cstring; // freigeben
    }
    rule_of_three(const rule_of_three& other) // II. Kopierkonstruktor
        : rule_of_three(other.cstring) {}
    rule_of_three& operator=(const rule_of_three& other) // III. Kopierzuweisung
    {
        // implementiert durch Kopieren-und-Tauschen der Kürze halber
        // beachte, dass dies potenzielle Wiederverwendung von Speicher verhindert
        rule_of_three temp(other);
        std::swap(cstring, temp.cstring);
        return *this;
    }
    const char* c_str() const // Zugriffsmethode
    {
        return cstring;
    }
};
int main()
{
    rule_of_three o1{"abc"};
    std::cout << o1.c_str() << ' ';
    auto o2{o1}; // II. verwendet Kopierkonstruktor
    std::cout << o2.c_str() << ' ';
    rule_of_three o3("def");
    std::cout << o3.c_str() << ' ';
    o3 = o2; // III. verwendet Kopierzuweisung
    std::cout << o3.c_str() << '\n';
}   // I. alle Destruktoren werden hier aufgerufen

Ausgabe:

abc abc def abc

Klassen, die nicht-kopierbare Ressourcen durch kopierbare Handles verwalten, müssen möglicherweise Kopierzuweisung und Kopierkonstruktor 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 Rule of Three: Das Löschen eines und das implizite Definieren des anderen ist typischerweise inkorrekt.

Regel der Fünf

Da das Vorhandensein eines benutzerdefinierten (einschließlich = default oder = delete deklarierten) Destruktors, Kopierkonstruktors oder Kopierzuweisungsoperators die implizite Definition des move constructor und des move assignment operator verhindert, muss jede Klasse, für die Move-Semantik erwünscht ist, alle fünf speziellen Member-Funktionen deklarieren:

class rule_of_five
{
    char* cstring; // Rohzeiger, verwendet als Handle für einen
                   // dynamisch allokierten Speicherblock
public:
    explicit rule_of_five(const char* s = "") : cstring(nullptr)
    { 
        if (s)
        {
            cstring = new char[std::strlen(s) + 1]; // allokieren
            std::strcpy(cstring, s); // befüllen
        { 
    }
    ~rule_of_five()
    {
        delete[] cstring; // deallokieren
    }
    rule_of_five(const rule_of_five& other) // Kopierkonstruktor
        : rule_of_five(other.cstring) {}
    rule_of_five(rule_of_five&& other) noexcept // Move-Konstruktor
        : cstring(std::exchange(other.cstring, nullptr)) {}
    rule_of_five& operator=(const rule_of_five& other) // Kopierzuweisung
    {
        // implementiert als Move-Zuweisung von einer temporären Kopie für Kürze
        // beachten Sie, dass dies potenzielle Wiederverwendung von Speicher verhindert
        return *this = rule_of_five(other);
    }
    rule_of_five& operator=(rule_of_five&& other) noexcept // Move-Zuweisung
    {
        std::swap(cstring, other.cstring);
        return *this;
    }
// alternativ können beide Zuweisungsoperatoren durch eine Copy-and-Swap-
// Implementierung ersetzt werden, die ebenfalls keine Wiederverwendung
// von Speicher in der Kopierzuweisung ermöglicht.
//  rule_of_five& operator=(rule_of_five other) noexcept
//  {
//      std::swap(cstring, other.cstring);
//      return *this;
//  }
};

Im Gegensatz zur Rule of Three ist das Unterlassen der Bereitstellung eines Move-Konstruktors und einer Move-Zuweisung in der Regel kein Fehler, sondern eine verpasste Optimierungsmöglichkeit.

Regel von Null

Klassen, die benutzerdefinierte Destruktoren, Kopier-/Move-Konstruktoren oder Kopier-/Move-Zuweisungsoperatoren besitzen, sollten sich ausschließlich mit Ownership befassen (was aus dem Single Responsibility Principle folgt). Andere Klassen sollten keine benutzerdefinierten Destruktoren, Kopier-/Move-Konstruktoren oder Kopier-/Move-Zuweisungsoperatoren besitzen [1] .

Diese Regel erscheint auch in den C++ Core Guidelines als C.20: If you can avoid defining default operations, do .

class rule_of_zero
{
    std::string cppstring;
public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
**Anmerkung:** Der gesamte Code innerhalb der `
` Tags wurde gemäß den Anweisungen nicht übersetzt, da es sich um C++-Code handelt. Die HTML-Struktur und Attribute wurden ebenfalls unverändert beibehalten.

Wenn eine Basisklasse für polymorphe Verwendung vorgesehen ist, muss ihr Destruktor möglicherweise als public und virtual deklariert werden. Dies unterbindet implizite Moves (und verwirft implizite Kopien), daher müssen die speziellen Elementfunktionen als = default [2] definiert werden.

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;
};

Allerdings macht dies die Klasse anfällig für Slicing, weshalb polymorphe Klassen das Kopieren oft als = delete definieren (siehe C.67: A polymorphic class should suppress public copy/move in den C++ Core Guidelines), was zur folgenden generischen Formulierung für die Rule of Five führt:

C.21: Wenn Sie eine Copy-, Move- oder Destruktor-Funktion definieren oder =delete verwenden, definieren oder =delete Sie alle.

Externe Links

  1. "Rule of Zero", R. Martinho Fernandes 15.08.2012
  2. "Eine Bedenken bezüglich der Rule of Zero", Scott Meyers, 13.03.2014 .