Suche…


Fünfter Regel

C ++ 11

C ++ 11 führt zwei neue spezielle Memberfunktionen ein: den Bewegungskonstruktor und den Bewegungszuweisungsoperator. Aus den gleichen Gründen, die Sie der Drei-Regel in C ++ 03 folgen möchten, möchten Sie normalerweise der Fünf-Regel in C ++ 11 folgen: Wenn eine Klasse EINE von fünf speziellen Member-Funktionen erfordert, und verschieben Sie die Semantik erwünscht sind, dann werden höchstwahrscheinlich ALLE FÜNF von ihnen benötigt.

Beachten Sie jedoch, dass das Nichtbefolgen der Fünf-Regel normalerweise nicht als Fehler, sondern als verpasste Optimierungsmöglichkeit betrachtet wird, solange die Drei-Regel weiterhin befolgt wird. Wenn kein Verschiebungskonstruktor oder Verschiebungszuweisungsoperator verfügbar ist, wenn der Compiler normalerweise einen verwenden würde, verwendet er stattdessen die Kopiersemantik, was zu einer weniger effizienten Operation aufgrund unnötiger Kopiervorgänge führt. Wenn für eine Klasse keine Verschiebesemantik gewünscht wird, muss kein Verschiebungskonstruktor oder Zuweisungsoperator deklariert werden.

Gleiches Beispiel wie für die Dreierregel:

class Person
{
    char* name;
    int age;

public:
    // Destructor 
    ~Person() { delete [] name; }

    // Implement Copy Semantics
    Person(Person const& other)
        : name(new char[std::strlen(other.name) + 1])
        , age(other.age)
    {
        std::strcpy(name, other.name);
    }
    
    Person &operator=(Person const& other) 
    {
        // Use copy and swap idiom to implement assignment.
        Person copy(other);
        swap(*this, copy);
        return *this;
    }

    // Implement Move Semantics
    // Note: It is usually best to mark move operators as noexcept
    //       This allows certain optimizations in the standard library
    //       when the class is used in a container.

    Person(Person&& that) noexcept
        : name(nullptr)               // Set the state so we know it is undefined
        , age(0)
    {
        swap(*this, that);
    }

    Person& operator=(Person&& that) noexcept
    {
        swap(*this, that);
        return *this;
    }

    friend void swap(Person& lhs, Person& rhs) noexcept
    {
        std::swap(lhs.name, rhs.name);
        std::swap(lhs.age, rhs.age);
    }
};

Alternativ können sowohl der Kopier- als auch der Verschiebungszuweisungsoperator durch einen einzelnen Zuweisungsoperator ersetzt werden, der anstelle des Verweises oder des R-Werts einen Wert als Instanz verwendet, um die Verwendung des Copy-and-Swap-Idioms zu erleichtern.

Person& operator=(Person copy)
{
    swap(*this, copy);
    return *this;
}

Die Erweiterung von der Drei-Regel auf die Fünf-Regel ist aus Leistungsgründen wichtig, aber in den meisten Fällen nicht unbedingt erforderlich. Durch das Hinzufügen des Kopierkonstruktors und des Zuweisungsoperators wird sichergestellt, dass durch das Verschieben des Typs kein Speicherplatzverlust entsteht (in diesem Fall greift Move-Construing einfach auf das Kopieren zurück), führt jedoch Kopien aus, die der Aufrufer wahrscheinlich nicht erwartet hat.

Nullregel

C ++ 11

Wir können die Prinzipien der Regel von Fünf und von RAII kombinieren, um eine viel schlankere Schnittstelle zu erhalten: Die Regel von Null: Jede Ressource, die verwaltet werden muss, sollte in einem eigenen Typ sein. Dieser Typ muss der Fünf-Regel folgen, aber alle Benutzer dieser Ressource müssen keine der fünf speziellen Member-Funktionen schreiben und können einfach alle als default festlegen.

Mit der im Drei-Regel-Beispiel eingeführten Person Klasse können wir ein Ressourcenverwaltungsobjekt für cstrings :

class cstring {
private:
    char* p;

public:
    ~cstring() { delete [] p; }
    cstring(cstring const& );
    cstring(cstring&& );
    cstring& operator=(cstring const& );
    cstring& operator=(cstring&& );

    /* other members as appropriate */
};

Und wenn dies einmal getrennt ist, wird unsere Person Klasse viel einfacher:

class Person {
    cstring name;
    int arg;

public:
    ~Person() = default;
    Person(Person const& ) = default;
    Person(Person&& ) = default;
    Person& operator=(Person const& ) = default;
    Person& operator=(Person&& ) = default;

    /* other members as appropriate */
};

Die besonderen Mitglieder in Person müssen nicht einmal ausdrücklich deklariert werden; Der Compiler setzt sie entsprechend dem Inhalt von Person standardmäßig zurück oder löscht sie entsprechend. Daher ist das Folgende auch ein Beispiel für die Nullregel.

struct Person {
    cstring name;
    int arg;
};

Wenn es sich bei cstring um einen Nur-Move-Typ mit einem Konstruktor / Zuweisungsoperator zum delete Kopien handeln würde, würde Person automatisch ebenfalls nur Move sein.

Die Term-Null-Regel wurde von R. Martinho Fernandes eingeführt

Dreierregel

c ++ 03

Die Dreierregel besagt, dass, wenn ein Typ jemals über einen benutzerdefinierten Kopierkonstruktor, einen Kopierzuweisungsoperator oder einen Destruktor verfügen muss, alle drei vorhanden sein müssen .

Der Grund für die Regel ist, dass eine Klasse, die eine der drei Klassen benötigt, eine Ressource (Dateizugriffsnummern, dynamisch zugewiesenen Speicher usw.) verwaltet, und alle drei werden benötigt, um diese Ressource konsistent zu verwalten. Die Kopierfunktionen behandeln, wie die Ressource zwischen Objekten kopiert wird, und der Destruktor würde die Ressource gemäß den RAII-Prinzipien zerstören.

Betrachten Sie einen Typ, der eine Zeichenfolgenressource verwaltet:

class Person
{
    char* name;
    int age;

public:
    Person(char const* new_name, int new_age)
        : name(new char[std::strlen(new_name) + 1])
        , age(new_age)
    {
       std::strcpy(name, new_name);
    }

    ~Person() {
        delete [] name;
    }
};

Da der name im Konstruktor vergeben wurde, wird der Destruktor freigegeben, um Speicherlecks zu vermeiden. Was passiert aber, wenn ein solches Objekt kopiert wird?

int main()
{
    Person p1("foo", 11);
    Person p2 = p1;
}

Zuerst wird p1 konstruiert. Dann wird p2 von p1 kopiert. Der von C ++ generierte Kopierkonstruktor kopiert jedoch jede Komponente des Typs unverändert. Was bedeutet, dass p1.name und p2.name auf dieselbe Zeichenfolge verweisen.

Wenn main endet, werden Destruktoren aufgerufen. Der erste Destruktor von p2 wird aufgerufen. Die Zeichenfolge wird gelöscht. Dann wird der Destruktor von p1 aufgerufen. Die Zeichenfolge ist jedoch bereits gelöscht . Das Aufrufen von delete in bereits gelöschtem Speicher führt zu undefiniertem Verhalten.

Um dies zu vermeiden, muss ein geeigneter Copy-Konstruktor bereitgestellt werden. Ein Ansatz besteht darin, ein Referenzzählsystem zu implementieren, bei dem verschiedene Person dieselben Zeichenfolgendaten verwenden. Bei jeder Ausführung einer Kopie wird der gemeinsame Referenzzähler erhöht. Der Destruktor verringert dann die Referenzzählung und gibt den Speicher nur dann frei, wenn die Zählung Null ist.

Oder wir könnten Wertsemantik und tiefes Kopierverhalten implementieren:

Person(Person const& other)
    : name(new char[std::strlen(other.name) + 1])
    , age(other.age)
{
    std::strcpy(name, other.name);
}

Person &operator=(Person const& other) 
{
    // Use copy and swap idiom to implement assignment
    Person copy(other);
    swap(copy);            //  assume swap() exchanges contents of *this and copy
    return *this;
}

Die Implementierung des Kopierzuweisungsoperators wird durch die Notwendigkeit der Freigabe eines vorhandenen Puffers erschwert. Die Copy- und Swap-Technik erstellt ein temporäres Objekt, das einen neuen Puffer enthält. Durch das Austauschen des Inhalts von *this und copy das Eigentum an der copy des ursprünglichen Puffers erhalten. Bei der Zerstörung einer copy wird der Puffer, der zuvor von *this besessen wurde, *this .

Selbstzuteilung Schutz

Beim Schreiben eines Kopierzuweisungsoperators ist es sehr wichtig, dass er bei Selbstzuweisung arbeiten kann. Das heißt, es muss dies zulassen:

SomeType t = ...;
t = t;

Selbstzuteilung geschieht normalerweise nicht so offensichtlich. Es geschieht in der Regel über einen Umweg durch verschiedene Kontrollsysteme, in denen die Lage der Zuordnung hat einfach zwei Person Zeiger oder Verweise und hat keine Ahnung , dass sie das gleiche Objekt sind.

Jeder Kopierzuweisungsoperator, den Sie schreiben, muss dies berücksichtigen können.

Die typische Methode ist, die Zuweisungslogik in eine Bedingung wie folgt zu hüllen:

SomeType &operator=(const SomeType &other)
{
    if(this != &other)
    {
        //Do assignment logic.
    }
    return *this;
}

Hinweis: Es ist wichtig, über die Selbstzuweisung nachzudenken und sicherzustellen, dass sich Ihr Code bei Auftreten korrekt verhält. Selbstzuweisung ist jedoch ein sehr seltenes Ereignis und die Optimierung, um zu verhindern, dass dies den Normalfall pessimiert. Da der Normalfall sehr viel häufiger vorkommt, kann die Verbesserung der Code-Effizienz durch die Pessimierung der Selbstzuweisung sehr beeinträchtigt werden.

Die übliche Technik zum Implementieren des Zuweisungsoperators ist beispielsweise das copy and swap idiom . Die normale Implementierung dieser Technik macht sich nicht die Mühe, die Selbstzuweisung zu testen (obwohl Selbstzuweisung teuer ist, weil eine Kopie erstellt wird). Der Grund ist, dass die Pessimisierung des Normalfalls viel kostspieliger ist (da dies öfter geschieht).

c ++ 11

Verschiebungszuweisungsoperatoren müssen auch vor Selbstzuweisung geschützt werden. Die Logik für viele dieser Operatoren basiert jedoch auf std::swap , das das Auslagern aus / in den gleichen Speicher problemlos handhaben kann. Wenn Ihre Zugzuweisungslogik nichts anderes als eine Reihe von Swap-Operationen ist, brauchen Sie keinen Schutz vor Selbstzuweisung.

Ist dies nicht der Fall, müssen Sie ähnliche Maßnahmen wie oben einleiten.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow