Pytanie Czym jest idiom kopiowania i wymiany?


Co to za idiom i kiedy należy go używać? Jakie problemy rozwiązuje? Czy idiom zmienia się, gdy używany jest C ++ 11?

Chociaż wspomniano o nim w wielu miejscach, nie mieliśmy żadnego szczególnego pytania i odpowiedzi, więc oto jest. Oto częściowa lista miejsc, w których wcześniej wspomniano:


1671
2017-07-19 08:42


pochodzenie


gotw.ca/gotw/059.htm od Herb Sutter - DumbCoder
Wspaniale, połączyłem to pytanie z moim odpowiedź, aby przenieść semantykę. - fredoverflow
Dobry pomysł na pełne wyjaśnienie tego idiomu, jest tak powszechny, że każdy powinien o tym wiedzieć. - Matthieu M.
Ostrzeżenie: Idiom kopiowania / wymiany jest wykorzystywany znacznie częściej niż jest to użyteczne. Często jest to szkodliwe dla wydajności, gdy nie jest wymagana silna gwarancja bezpieczeństwa wyjątku od przypisania kopii. A gdy wymagane jest silne wyjątkowe bezpieczeństwo przy kopiowaniu, można je łatwo uzyskać dzięki krótkiej ogólnej funkcji, a także znacznie szybszemu operatorowi przydziału kopii. Widzieć slideshare.net/ripplelabs/howard-hinnant-accu2014 slajdy 43 - 53. Podsumowanie: kopiowanie / zamiana jest użytecznym narzędziem w przyborniku. Ale był zbyt duży, a następnie często był nadużywany. - Howard Hinnant
@HowardHinnant: Tak, +1 do tego. Napisałem to w czasie, gdy prawie każde pytanie w C ++ brzmiało "pomóc mojej klasie się zawiesić, kiedy ją skopiowałem" i to była moja odpowiedź. Jest to właściwe, gdy chcesz po prostu pracować z semantyką kopiowania / przenoszenia lub cokolwiek innego, abyś mógł przejść do innych rzeczy, ale nie jest to optymalne. Jeśli uważasz, że to pomoże, możesz złożyć zastrzeżenie na górze mojej odpowiedzi. - GManNickG


Odpowiedzi:


Przegląd

Dlaczego potrzebujemy idiomu kopiowania i zamiany?

Każda klasa zarządzająca zasobami (a obwoluta, podobnie jak inteligentny wskaźnik) musi zostać wdrożony Wielka trójka. Podczas gdy cele i implementacja konstruktora kopii i destruktora są proste, operator kopiowania jest prawdopodobnie najbardziej skomplikowany i trudny. Jak to zrobić? Jakie pułapki należy unikać?

The idiom kopiowania i zamiany jest rozwiązaniem i elegancko pomaga operatorowi przydziału w osiąganiu dwóch rzeczy: unikaniu duplikacja kodui zapewnienie silna gwarancja wyjątku.

Jak to działa?

Koncepcyjniedziała za pomocą funkcji konstruktora kopiowania w celu utworzenia lokalnej kopii danych, a następnie pobiera skopiowane dane za pomocą swap funkcja, zamiana starych danych na nowe dane. Tymczasowa kopia ulega zniszczeniu, zabierając ze sobą stare dane. Zostaje nam kopia nowych danych.

Aby użyć idiomu kopiowania i zamiany, potrzebujemy trzech rzeczy: działającego konstruktora kopii, działającego destruktora (oba są podstawą dowolnego opakowania, więc i tak powinny być kompletne), a swap funkcjonować.

Funkcja wymiany to nie rzucając funkcja, która zamienia dwa obiekty klasy, członek dla członka. Możemy ulec pokusie użycia std::swap zamiast dostarczać własne, ale to byłoby niemożliwe; std::swap korzysta z konstruktora kopiowania i operatora przydziału kopiowania w ramach jego implementacji, a my ostatecznie spróbujemy zdefiniować operatora przypisania w kategoriach samego siebie!

(Nie tylko to, ale niewykwalifikowane rozmowy do swap użyje naszego niestandardowego operatora wymiany, pomijając niepotrzebną konstrukcję i niszczenie naszej klasy std::swap pociągałoby za sobą.)


Dogłębne wyjaśnienie

Cel

Rozważmy konkretny przypadek. Chcemy zarządzać, w klasie bezużytecznej, tablicą dynamiczną. Zaczynamy od działającego konstruktora, konstruktora kopiowania i destruktora:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Ta klasa pomyślnie zarządza macierzą, ale jej potrzebuje operator= działać poprawnie.

Nieudane rozwiązanie

Oto jak może wyglądać naiwna implementacja:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

I mówimy, że jesteśmy skończeni; to teraz zarządza układem, bez przecieków. Jednak ma trzy problemy, oznaczone sekwencyjnie w kodzie jako (n).

  1. Pierwszym z nich jest test samozatrudnienia. Kontrola ta służy dwóm celom: jest to prosty sposób, aby uniemożliwić nam uruchomienie niepotrzebnego kodu na własny rachunek, i chroni nas przed subtelnymi błędami (takimi jak usuwanie tablicy tylko w celu jej wypróbowania i skopiowania). Ale we wszystkich innych przypadkach służy jedynie spowolnieniu programu i działa jako szum w kodzie; samo-przydział rzadko się zdarza, więc przez większość czasu ta kontrola jest marnotrawstwem. Byłoby lepiej, gdyby operator mógł bez niego działać poprawnie.

  2. Po drugie zapewnia jedynie podstawową gwarancję wyjątku. Gdyby new int[mSize] zawiedzie, *this zostanie zmodyfikowany. (Mianowicie, rozmiar jest nieprawidłowy, a dane już nie ma!) W przypadku gwarancji wyjątkowej, musi to być coś podobnego do:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. Kod został rozszerzony! Co prowadzi nas do trzeciego problemu: powielania kodu. Nasz operator przydziału skutecznie powiela cały kod, który napisaliśmy już gdzie indziej, a to jest okropne.

W naszym przypadku jego rdzeniem są tylko dwie linie (przydział i kopia), ale przy bardziej złożonych zasobach ten kod może być dość kłopotliwy. Powinniśmy starać się nigdy nie powtarzać.

(Ktoś mógłby się zastanawiać: czy ten kod jest potrzebny do prawidłowego zarządzania jednym zasobem, a co jeśli moja klasa zarządza więcej niż jednym? Chociaż może to wydawać się ważną kwestią, a nawet wymaga nietrywialnego try/catch klauzule, to nie jest problem. To dlatego, że klasa powinna sobie poradzić tylko jeden zasób!)

Udane rozwiązanie

Jak wspomniano, idiom kopiowania i wymiany rozwiąże wszystkie te problemy. Ale teraz mamy wszystkie wymagania z wyjątkiem jednego: swap funkcjonować. Podczas gdy Reguła Trzech z powodzeniem wiąże się z istnieniem naszego konstruktora kopiowania, operatora przypisania i destruktora, powinna być naprawdę nazwana "Wielka trójka i połowa": za każdym razem, gdy twoja klasa zarządza zasobem, ma sens również zapewnienie swap funkcjonować.

Musimy dodać funkcję wymiany do naszej klasy i robimy to w następujący sposób †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

(Tutaj jest wyjaśnienie, dlaczego public friend swap.) Teraz możemy nie tylko wymienić dumb_arrayale ogólnie rzecz biorąc swapy mogą być bardziej wydajne; zamienia tylko wskaźniki i rozmiary, zamiast przydzielać i kopiować całe tablice. Oprócz tego bonusu w zakresie funkcjonalności i wydajności, jesteśmy teraz gotowi do wdrożenia idiomu kopiowania i zamiany.

Bez dalszych ceregieli nasz operator przydziału jest:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

I to wszystko! Za jednym zamachem wszystkie trzy problemy są od razu elegancko rozwiązane.

Dlaczego to działa?

Zauważamy najpierw ważny wybór: argument parametru jest podejmowany wartość podrzędna. Podczas gdy można równie łatwo wykonać następujące czynności (a nawet, na wiele naiwnych implementacji tego idiomu):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Tracimy ważna możliwość optymalizacji. Nie tylko to, ale ten wybór jest krytyczny w C ++ 11, o czym jest mowa później. (Ogólnie rzecz biorąc, niezwykle użyteczna wytyczna jest następująca: jeśli zamierzasz zrobić kopię czegoś w funkcji, pozwól, aby kompilator zrobił to na liście parametrów.)

Tak czy inaczej, ta metoda uzyskiwania naszego zasobu jest kluczem do wyeliminowania powielania kodu: możemy użyć kodu z konstruktora kopiowania, aby wykonać kopię i nigdy nie trzeba jej powtarzać. Po wykonaniu kopii jesteśmy gotowi do wymiany.

Zwróć uwagę, że po wejściu do funkcji wszystkie nowe dane są już przydzielone, skopiowane i gotowe do użycia. To daje nam silną gwarancję wyjątkową za darmo: nie wejdziemy nawet w funkcję, jeśli konstrukcja kopii nie powiedzie się, a zatem nie można zmienić stanu *this. (To, co zrobiliśmy wcześniej ręcznie, aby uzyskać silną gwarancję wyjątku, kompilator robi dla nas teraz, jakże miły).

W tym momencie jesteśmy wolni od domu, ponieważ swap nie rzuca się. Zamieniamy nasze bieżące dane na kopiowane dane, bezpiecznie zmieniając nasz stan, a stare dane wprowadzamy do tymczasowego. Stare dane zostaną zwolnione po powrocie funkcji. (Gdzie kończy się zakres parametru i wywoływany jest jego destruktor).

Ponieważ idiom nie powtarza żadnego kodu, nie możemy wprowadzać błędów w obrębie operatora. Zauważ, że oznacza to, że pozbywamy się potrzeby sprawdzania własnej przynależności, umożliwiając jednolitą implementację operator=. (Dodatkowo, nie mamy już kary za wydajność w przypadku nieautoryzowanych zadań.)

I to jest idiom kopiowania i zamiany.

A co z C ++ 11?

Kolejna wersja C ++, C ++ 11, stanowi jedną bardzo ważną zmianę w sposobie zarządzania zasobami: teraz obowiązuje zasada trzech Zasada Czterech (i pół). Czemu? Ponieważ nie tylko musimy mieć możliwość kopiowania - konstruowania naszego zasobu, musimy też ruszyć - skonstruować to.

Na szczęście dla nas jest to łatwe:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Co tu się dzieje? Przypomnij sobie cel budowy ruchu: przejmowanie zasobów z innej instancji klasy, pozostawiając ją w stanie gwarantującym możliwość przypisania i zniszczenia.

To, co zrobiliśmy, jest proste: zainicjuj za pomocą domyślnego konstruktora (funkcja C ++ 11), a następnie zamień z other; wiemy, że domyślnie skonstruowana instancja naszej klasy może być bezpiecznie przypisana i zniszczona, więc wiemy other będzie mógł zrobić to samo, po zamianie.

(Zauważ, że niektóre kompilatory nie obsługują delegowania konstruktora, w tym przypadku musimy ręcznie utworzyć domyślną klasę, co jest niefortunnym, ale na szczęście trywialnym zadaniem).

Dlaczego to działa?

To jedyna zmiana, którą musimy wprowadzić w naszej klasie, więc dlaczego to działa? Pamiętaj, że zawsze ważna decyzja, którą podjęliśmy, aby parametr był wartością, a nie referencją:

dumb_array& operator=(dumb_array other); // (1)

Teraz jeśli other jest inicjowany z wartością r, będzie zbudowany z ruchu. Idealny. W ten sam sposób C ++ 03 pozwala nam ponownie użyć naszej funkcji konstruktora kopii poprzez przyjęcie argumentu by-value, C ++ 11 automatycznie wybierz konstruktor ruchu, jeśli jest to odpowiednie. (I, oczywiście, jak wspomniano w poprzednio powiązanym artykule, kopiowanie / przenoszenie wartości może być po prostu całkowicie anulowane.)

I tak kończy się idiom kopiowania i zamiany.


Przypisy

* Dlaczego ustawiamy mArray do null? Ponieważ jeśli jakiś dalszy kod w operator rzuca, destruktor dumb_array można nazwać; a jeśli tak się stanie bez ustawienia na wartość null, próbujemy usunąć pamięć, która została już usunięta! Unikamy tego, ustawiając wartość null, ponieważ usunięcie wartości null jest bezczynne.

† Są inne roszczenia, które powinniśmy specjalizować std::swap dla naszego typu, zapewniaj w klasie swap obok bezpłatnej funkcji swap, itp. Ale to wszystko jest niepotrzebne: jakiekolwiek właściwe użycie swap będzie przez niewykwalifikowany telefon, a nasza funkcja zostanie znaleziona przez ADL. Zrobi się jedna funkcja.

‡ Powód jest prosty: gdy masz już swój zasób, możesz go zamienić i / lub przenieść (C ++ 11) w dowolne miejsce. Wykonując kopię na liście parametrów, zmaksymalizujesz optymalizację.


1841
2017-07-19 08:43



@GMan: Twierdzę, że klasa zarządzająca kilkoma zasobami jednocześnie jest skazana na niepowodzenie (wyjątkowe bezpieczeństwo staje się koszmarne) i zdecydowanie zaleciłbym, aby klasa zarządzała JEDNIM zasobem LUB posiadała funkcjonalność biznesową i korzystała z menedżerów. - Matthieu M.
@FrEEzE: "Jest to specyficzne dla kompilatora, w jakiej kolejności przetwarzana jest lista." Nie, nie jest. Jest przetwarzany w kolejności, w jakiej pojawiają się w definicji klasy. Kompilator, który nie akceptuje std::copy ta droga jest zepsuta, nie koduję uszkodzonych kompilatorów. I nie jestem pewien, czy rozumiem twój ostatni komentarz. - GManNickG
@Freeze: Poza tym celem tej odpowiedzi jest mówienie o idiomie C ++. Jeśli chcesz zhackować swój program do pracy z niezgodnym kompilatorem, to dobrze, ale nie staraj się postępować tak, jak to jest moja odpowiedzialność lub, że nie jest to "zła praktyka", proszę. - GManNickG
Nie rozumiem, dlaczego metoda zamiany jest tutaj zadeklarowana jako przyjaciel? - szx
@neuviemeporte: Potrzebujesz swojego swap można znaleźć podczas ADL, jeśli chcesz, aby działał w najbardziej ogólnym kodzie, na który napotkasz, np boost::swap i inne różne instancje wymiany. Zamiana jest trudnym problemem w C ++ i ogólnie wszyscy zgadzamy się, że jeden punkt dostępu jest najlepszy (dla spójności), a jedynym sposobem na zrobienie tego w ogóle jest funkcja bezpłatna (int nie może mieć na przykład członka zamiany). Widzieć moje pytanie na jakieś tło. - GManNickG


Zadanie w jego sercu to dwa kroki: zburzenie starego stanu obiektu i budowanie nowego stanu jako kopii stanu innego obiektu.

Zasadniczo, właśnie to burzyciel i skopiować konstruktora tak, więc pierwszym pomysłem byłoby przekazanie im pracy. Jednakże, ponieważ zniszczenie nie może zawieść, podczas gdy budowa może, naprawdę chcemy to zrobić na odwrót: najpierw wykonaj część konstrukcyjną a jeśli to się uda, następnie wykonaj destrukcyjną część. Idiom kopiowania i wymiany jest sposobem, aby to zrobić: Najpierw wywołuje konstruktor klasy "copy", aby utworzyć tymczasowy, a następnie zamienia swoje dane na pliki tymczasowe, a następnie pozwala niszczycielowi tymczasowemu zniszczyć stary stan.
Od swap() ma nigdy nie zawieść, jedyną częścią, która może zawieść, jest konstrukcja kopii. Jest to wykonywane w pierwszej kolejności, a jeśli się nie powiedzie, nic nie zostanie zmienione w docelowym obiekcie.

W udoskonalonej formie, kopiowanie i zamiana jest realizowane poprzez wykonanie kopii przez zainicjowanie parametru (niereferencyjnego) operatora przypisania:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

227
2017-07-19 08:55



Myślę, że wspomnienie o pimpl jest równie ważne, jak wspomnienie kopii, zamiany i zniszczenia. Ta zamiana nie jest magicznie wyjątkowa - bezpieczna. Jest wyjątkowo bezpieczny, ponieważ wskaźniki zamiany są wyjątkowo bezpieczne. Ty nie mieć aby użyć pimpl, ale jeśli tego nie zrobisz, musisz upewnić się, że każda wymiana członka jest wyjątkowa. To może być koszmar, kiedy ci członkowie mogą się zmienić i jest to banalne, gdy są schowani za pimplem. A potem pojawia się koszt pimpl. Co prowadzi nas do wniosku, że często bezpieczeństwo wyjątków wiąże się z kosztami wydajności. - wilhelmtell
std::swap(this_string, that) nie zapewnia gwarancji braku rzutu. Zapewnia silne bezpieczeństwo wyjątkowe, ale nie gwarantuje braku rzutu. - wilhelmtell
@wilhelmtell: W C ++ 03 nie ma wzmianki o wyjątkach potencjalnie wyrzucanych przez std::string::swap (która jest wywoływana przez std::swap). W C ++ 0x, std::string::swap jest noexcept i nie wolno wyrzucać wyjątków. - James McNellis
@ sbi @JamesMcNellis jest ok, ale punkt wciąż jest ważny: jeśli masz członków typu klasowego, upewnij się, że ich zamiana nie jest rzutem. Jeśli masz jednego członka, który jest wskaźnikiem, to jest to banalne. W przeciwnym razie nie jest. - wilhelmtell
@wilhelmtell: Myślałem, że to jest punkt wymiany: nigdy nie wyrzuca i zawsze jest O (1) (tak, wiem, std::array...) - sbi


Istnieje już kilka dobrych odpowiedzi. Skoncentruję się głównie na temat tego, co moim zdaniem im brakuje - wyjaśnienie "wad" z idiomem kopiowania i zamiany ....

Czym jest idiom kopiowania i wymiany?

Sposób implementacji operatora przypisania w zakresie funkcji wymiany:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

Podstawową ideą jest to, że:

  • najbardziej podatną na błędy częścią przypisywania do obiektu jest zapewnienie uzyskania zasobów, które są potrzebne do nowego stanu (na przykład pamięci, deskryptorów)

  • to przejęcie można próbować przed modyfikowanie bieżącego stanu obiektu (tj. *this) jeżeli kopia nowej wartości jest tworzona, dlatego rhs Jest zaakceptowane według wartości (tj. kopiowane), a nie przez odniesienie

  • zamiana stanu kopii lokalnej rhs i *this jest zazwyczaj stosunkowo łatwe do wykonania bez potencjalnych awarii / wyjątków, ponieważ kopia lokalna nie potrzebuje później żadnego konkretnego stanu (wystarczy, aby stan pasował do destruktora, tak jak w przypadku obiektu przeniósł from in = = C ++ 11)

Kiedy należy go użyć? (Jakie problemy rozwiązuje [/Stwórz]?)

  • Gdy chcesz, aby przypisany do obiektu obiekt był nienaruszony przez zadanie, które zgłasza wyjątek, zakładając, że masz lub możesz napisać swap z silną gwarancją wyjątku, a najlepiej taką, która nie może zawieść /throw.. †

  • Jeśli chcesz mieć czysty, łatwy do zrozumienia i solidny sposób definiowania operatora przypisania w kategoriach (prostszego) konstruktora kopiowania, swap i funkcje destruktora.

    • Samozatrudnienie wykonane jako kopiowanie i zamiana pozwala uniknąć przeoczonych przypadków krawędzi. ‡

  • Kiedy jakakolwiek kara za wydajność lub chwilowo większe zużycie zasobów, stworzone przez posiadanie dodatkowego obiektu tymczasowego podczas przydzielania, nie jest ważne dla twojej aplikacji. ⁂

swap rzucanie: ogólnie możliwe jest niezawodnie zamieniać elementy danych, które obiekty śledzą za pomocą wskaźnika, ale elementy danych nieprzypadkowe, które nie mają swapu wolnego od rzutów lub dla których zamiana musi być wykonana jako X tmp = lhs; lhs = rhs; rhs = tmp; a konstrukcja kopii lub cesja może rzucić, wciąż może się nie udać, pozostawiając niektórych członków zamienionych, a inni nie. Ten potencjał dotyczy nawet C ++ 03 std::stringtak jak James komentuje inną odpowiedź:

@wilhelmtell: W C ++ 03 nie ma wzmianki o wyjątkach potencjalnie wyrzucanych przez std :: string :: swap (który jest wywoływany przez std :: swap). W C ++ 0x, std :: string :: swap nie jest wyjątkiem i nie może zgłaszać wyjątków. - James McNellis 22 grudnia 10 o 15:24


‡ implementacja operatora przypisania, która wydaje się rozsądna, gdy przypisanie z odrębnego obiektu może łatwo zakończyć się niepowodzeniem w celu samodzielnego przypisania. Chociaż może wydawać się niewyobrażalne, że kod klienta próbowałby nawet samozasłonięcia, może się to zdarzyć stosunkowo łatwo podczas operacji algo na kontenerach, z x = f(x); kod gdzie f jest (być może tylko dla niektórych #ifdef gałęzie) a makro #define f(x) x lub funkcja zwracająca odwołanie do xlub nawet (prawdopodobnie nieskuteczny, ale zwięzły) kod jak x = c1 ? x * 2 : c2 ? x / 2 : x;). Na przykład:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Przy samodzielnym przypisaniu powyższy kod jest usuwany x.p_;, punkty p_ w nowo przydzielonym regionie sterty, a następnie próbuje odczytać niezainicjowany dane w nim (niezdefiniowane zachowanie), jeśli to nie robi nic zbyt dziwnego, copy usiłuje samozwańczy do każdego just-destructed "T"!


⁂ Idiom kopiowania i wymiany może wprowadzać nieefektywności lub ograniczenia wynikające z zastosowania dodatkowego tymczasowego (gdy parametr operatora jest konstruowany w postaci kopii):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Tutaj, odręczny Client::operator= może sprawdzić, czy *this jest już podłączony do tego samego serwera co rhs (być może wysłanie kodu "reset", jeśli jest użyteczne), podczas gdy metoda kopiowania i zamiany wywoływałaby konstruktora kopii, który prawdopodobnie zostałby napisany w celu otwarcia odrębnego połączenia przez gniazdo, a następnie zamknięcia oryginalnego. Nie tylko może to oznaczać zdalną interakcję sieciową, a nie zwykłą kopię zmiennej procesowej, ale może powodować ograniczenia klienta lub serwera dla zasobów gniazd lub połączeń. (Oczywiście ta klasa ma dość przerażający interfejs, ale to już inna kwestia ;-P).


32
2018-03-06 14:51



Powiedział, że połączenie z gniazdem to tylko przykład - ta sama zasada dotyczy potencjalnie kosztownej inicjalizacji, takiej jak sondowanie / inicjalizacja sprzętu, generowanie puli wątków lub liczb losowych, niektóre zadania kryptograficzne, pamięci podręczne, skanowanie systemu plików, baza danych Połączenia itp .. - Tony Delroy
Jest jeszcze jeden (masywny) con. Zgodnie z aktualnymi specyfikacjami technicznie obiekt będzie nie masz operatora przydziału przeniesienia! Jeśli później użyty jako członek klasy, nowa klasa nie będzie automatycznie generowany move-ctor! Źródło: youtu.be/mYrbivnruYw?t=43m14s - user362515
Główny problem z operatorem przypisania kopii Client czy to zadanie nie jest zabronione. - sbi


Ta odpowiedź jest raczej dodatkiem i niewielką modyfikacją powyższych odpowiedzi.

W niektórych wersjach Visual Studio (i prawdopodobnie innych kompilatorów) jest błąd, który jest naprawdę denerwujący i nie ma sensu. Więc jeśli zadeklarujesz / zdefiniujesz swoje swap działają tak:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... kompilator będzie krzyczeć na ciebie, gdy zadzwonisz swap funkcjonować:

enter image description here

Ma to coś wspólnego z friend funkcja jest wywoływana i this obiekt przekazany jako parametr.


Sposób obejścia tego jest nieużywany friend słowo kluczowe i przedefiniować swap funkcjonować:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Tym razem możesz po prostu zadzwonić swap i przekazać other, dzięki czemu kompilator jest szczęśliwy:

enter image description here


W końcu nie potrzeba używać a friend funkcja do zamiany 2 obiektów. Ma to tyle samo sensu swap funkcja członkowska, która ma jedną other obiekt jako parametr.

Masz już dostęp do this obiekt, więc przekazanie go jako parametru jest technicznie zbędne.


19
2017-09-04 04:50



Czy możesz udostępnić swój przykład, który odtwarza błąd? - GManNickG
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp  dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg. To jest wersja uproszczona. Błąd pojawia się za każdym razem friend funkcja jest wywoływana za pomocą *this parametr - Oleksiy
@GManNickG nie pasowałoby do komentarza zawierającego wszystkie obrazy i przykłady kodu. I jest ok, jeśli ludzie walczą, jestem pewien, że jest ktoś, kto dostaje ten sam błąd; informacje w tym poście mogą być właśnie tym, czego potrzebują. - Oleksiy
Zauważ, że jest to tylko błąd w podświetlaniu kodu IDE (IntelliSense) ... Kompiluje się dobrze bez ostrzeżeń / błędów. - Amro
Proszę zgłosić błąd VS tutaj, jeśli jeszcze tego nie zrobiłeś (i jeśli nie zostało to naprawione) connect.microsoft.com/VisualStudio - Matt


Chciałbym dodać słowo ostrzeżenia, gdy mamy do czynienia z pojemnikami w stylu C ++ w stylu alokatora. Zamiana i przypisanie mają subtelnie inną semantykę.

Dla konkretności rozważmy kontener std::vector<T, A>, gdzie A jest pewnym stanowym typem alokatora, a my porównamy następujące funkcje:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Celem obu funkcji fs i fm jest dawać a stan, który b miał początkowo. Istnieje jednak ukryte pytanie: co się stanie, jeśli a.get_allocator() != b.get_allocator()? Odpowiedź brzmi: to zależy. Napiszmy AT = std::allocator_traits<A>.

  • Gdyby AT::propagate_on_container_move_assignment jest std::true_type, następnie fm przenosi przydział alokatora a z wartością b.get_allocator(), w przeciwnym razie nie, i a nadal korzysta z oryginalnego przydziału. W takim przypadku elementy danych muszą być zamieniane indywidualnie, ponieważ przechowywanie a i b nie jest kompatybilny.

  • Gdyby AT::propagate_on_container_swap jest std::true_type, następnie fs zamienia dane i alokatory w oczekiwany sposób.

  • Gdyby AT::propagate_on_container_swap jest std::false_type, potrzebujemy dynamicznego czeku.

    • Gdyby a.get_allocator() == b.get_allocator(), następnie dwa pojemniki korzystają z kompatybilnego magazynu, a zamiana przebiega w zwykły sposób.
    • Jeśli jednak a.get_allocator() != b.get_allocator(), program ma niezdefiniowane zachowanie (patrz [container.requirements.general / 8].

Efektem jest to, że zamiana stała się nietrywialną operacją w C ++ 11, gdy tylko twój kontener zacznie obsługiwać stanowe alokatory. To nieco "zaawansowany przypadek użycia", ale nie jest to całkowicie nieprawdopodobne, ponieważ optymalizacja ruchu zwykle staje się interesująca tylko wtedy, gdy klasa zarządza zasobem, a pamięć jest jednym z najpopularniejszych zasobów.


10
2018-06-24 08:16