Szukaj…


Reguła pięciu

C ++ 11

C ++ 11 wprowadza dwie nowe specjalne funkcje składowe: konstruktor ruchu i operator przypisania ruchu. Z tych samych powodów, dla których chcesz przestrzegać reguły trzech w C ++ 03, zwykle chcesz przestrzegać zasady pięciu w C ++ 11: jeśli klasa wymaga JEDNEJ z pięciu specjalnych funkcji składowych i jeśli przenosi semantykę są pożądane, to najprawdopodobniej wymaga WSZYSTKIEGO z nich.

Należy jednak pamiętać, że nieprzestrzeganie Reguły Pięciu zwykle nie jest uważane za błąd, ale za brak możliwości optymalizacji, o ile Reguła Trzech jest nadal przestrzegana. Jeśli żaden konstruktor ruchu lub operator przypisania ruchu nie jest dostępny, gdy kompilator normalnie użyje jednego, zamiast tego zastosuje semantykę kopiowania, jeśli to możliwe, co spowoduje mniej wydajną operację z powodu niepotrzebnych operacji kopiowania. Jeśli semantyka ruchu nie jest pożądana dla klasy, nie ma potrzeby deklarowania konstruktora ruchu lub operatora przypisania.

Taki sam przykład, jak w przypadku reguły trzech:

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

Alternatywnie zarówno operator przypisania kopiowania, jak i przeniesienia można zastąpić pojedynczym operatorem przypisania, który pobiera instancję według wartości zamiast odwołania lub wartości referencyjnej wartości w celu ułatwienia korzystania z idiomu kopiowania i zamiany.

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

Rozszerzenie zasady trzech do zasady pięciu jest ważne ze względu na wydajność, ale w większości przypadków nie jest absolutnie konieczne. Dodanie konstruktora kopiowania i operatora przypisania gwarantuje, że przeniesienie typu nie spowoduje przecieku pamięci (w tym przypadku po prostu cofnie się tworzenie kopii), ale wykona kopie, których osoba dzwoniąca prawdopodobnie nie spodziewała się.

Reguła zerowa

C ++ 11

Możemy połączyć zasady Rule of Five i RAII, aby uzyskać znacznie prostszy interfejs: Rule of Zero: każdy zasób, którym trzeba zarządzać, powinien być tego samego typu. Ten typ musiałby przestrzegać reguły pięciu, ale wszyscy użytkownicy tego zasobu nie muszą pisać żadnej z pięciu specjalnych funkcji składowych i mogą po prostu default wszystkie z nich.

Korzystając z klasy Person wprowadzonej w przykładzie Reguła trzech , możemy utworzyć obiekt zarządzający zasobami dla 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 */
};

A kiedy to się rozdzieli, nasza klasa Person staje się znacznie prostsza:

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

Specjalni członkowie Person nawet nie muszą być wyraźnie deklarowani; kompilator odpowiednio je domyślnie usunie lub usunie, w zależności od zawartości Person . Dlatego poniższe jest również przykładem reguły zerowej.

struct Person {
    cstring name;
    int arg;
};

Jeśli cstring miały być rodzajem ruch tylko, ze delete d kopiowania operatora konstruktor / przypisania, a następnie Person będzie automatycznie być ruch tylko jako dobrze.

Pojęcie reguły zero wprowadził R. Martinho Fernandes

Reguła Trzech

c ++ 03

Reguła trzech stwierdza, że jeśli typ musi kiedykolwiek mieć zdefiniowany przez użytkownika konstruktor kopii, operator przypisania kopii lub destruktor, wówczas musi mieć wszystkie trzy .

Powodem tej reguły jest to, że klasa, która potrzebuje któregokolwiek z trzech, zarządza jakimś zasobem (uchwyty plików, pamięć dynamicznie alokowana itp.) I wszystkie trzy są potrzebne do konsekwentnego zarządzania tym zasobem. Funkcje kopiowania radzą sobie ze sposobem kopiowania zasobu między obiektami, a destruktor zniszczyłby zasób, zgodnie z zasadami RAII .

Rozważ typ zarządzający zasobem łańcuchowym:

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

Ponieważ name została przydzielona w konstruktorze, destruktor zwalnia ją, aby uniknąć wycieku pamięci. Ale co się stanie, jeśli taki obiekt zostanie skopiowany?

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

Najpierw skonstruowane zostanie p1 . Następnie p2 zostanie skopiowane z p1 . Jednak generowany przez C ++ konstruktor kopiowania skopiuje każdy komponent typu „tak jak jest”. Co oznacza, że p1.name i p2.name oba wskazują ten sam ciąg.

Kiedy main kończą się, wywołane zostaną destruktory. Pierwszy destruktor p2 zostanie wywołany; usunie ciąg. Następnie zostanie wywołany destruktor p1 . Jednak ciąg jest już usunięty . Wywołanie funkcji delete w pamięci, która została już usunięta, powoduje niezdefiniowane zachowanie.

Aby tego uniknąć, konieczne jest zapewnienie odpowiedniego konstruktora kopii. Jednym z podejść jest wdrożenie systemu zliczania referencji, w którym różne instancje Person współużytkują ten sam ciąg danych. Za każdym razem, gdy wykonywana jest kopia, liczba współdzielonych odniesień jest zwiększana. Destruktor następnie zmniejsza liczbę referencyjną, zwalniając pamięć tylko wtedy, gdy liczba wynosi zero.

Lub możemy zastosować semantykę wartości i zachowanie do głębokiego kopiowania :

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

Wdrożenie operatora przypisania kopii jest skomplikowane z powodu konieczności zwolnienia istniejącego bufora. Technika kopiowania i zamiany tworzy obiekt tymczasowy, który przechowuje nowy bufor. Zamiana zawartości *this i copy daje własność copy oryginalnego bufora. Zniszczenie copy , gdy funkcja powraca, zwalnia bufor poprzednio posiadany przez *this .

Ochrona przydziałów

Podczas pisania operatora przypisania kopii bardzo ważne jest, aby mógł on działać w przypadku samodzielnego przypisania. Oznacza to, że musi na to pozwolić:

SomeType t = ...;
t = t;

Samoprzypisanie zwykle nie dzieje się w tak oczywisty sposób. Zwykle dzieje się to okrężną trasą przez różne systemy kodowania, w których lokalizacja przypisania ma po prostu dwa wskaźniki Person lub odniesienia i nie ma pojęcia, że są one tym samym obiektem.

Każdy operator przypisania kopii, który napiszesz, musi być w stanie to uwzględnić.

Typowym sposobem jest zawinięcie całej logiki przypisania w taki stan:

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

Uwaga: Ważne jest, aby pomyśleć o samodzielnym przypisaniu i upewnić się, że kod zachowuje się poprawnie, gdy to nastąpi. Jednak samodzielne przypisanie jest bardzo rzadkim zjawiskiem i optymalizacja w celu zapobieżenia może w rzeczywistości pesymalizować normalny przypadek. Ponieważ normalny przypadek jest znacznie bardziej powszechny, pesymizacja samodzielnego przypisania może znacznie zmniejszyć wydajność kodu (więc ostrożnie go używaj).

Na przykład normalną techniką implementacji operatora przypisania jest copy and swap idiom . Normalne wdrożenie tej techniki nie przeszkadza w testowaniu samodzielnego przypisania (nawet jeśli samodzielne przypisanie jest kosztowne, ponieważ tworzona jest kopia). Powodem jest to, że pesymizacja normalnego przypadku okazała się znacznie bardziej kosztowna (ponieważ zdarza się to częściej).

c ++ 11

Operatorzy przypisań do ruchu również muszą być chronieni przed samodzielnym przypisaniem. Jednak logika wielu takich operatorów opiera się na std::swap , który dobrze radzi sobie z zamianą z / do tej samej pamięci. Jeśli więc logika przypisania do ruchu jest niczym więcej niż serią operacji zamiany, nie potrzebujesz ochrony przed przypisaniem.

Jeśli tak nie jest, musisz podjąć podobne środki jak powyżej.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow