Co to jest idiom kopiowania i zamiany?

1999

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

Chociaż w wielu miejscach wspomniano o tym, nie mieliśmy żadnego pojedynczego pytania i odpowiedzi „co to jest”, więc oto jest. Oto częściowa lista miejsc, w których wcześniej wspomniano:

GManNickG
źródło
7
gotw.ca/gotw/059.htm z Herb Sutter
DumbCoder
2
Wspaniale, połączyłem to pytanie z mojej odpowiedzi, aby przenieść semantykę .
fredoverflow
4
Dobry pomysł, aby mieć pełne wyjaśnienie tego idiomu, jest tak powszechny, że wszyscy powinni o nim wiedzieć.
Matthieu M.,
16
Ostrzeżenie: idiom kopiowania / zamiany jest używany znacznie częściej niż jest użyteczny. Jest to często szkodliwe dla wydajności, gdy silna wyjątkowa gwarancja bezpieczeństwa nie jest wymagana od przypisania kopii. A gdy potrzebne jest silne wyjątkowe bezpieczeństwo przy przypisywaniu kopii, można je łatwo zapewnić za pomocą krótkiej funkcji ogólnej, a także o wiele szybszego operatora przypisywania kopii. Zobacz slideshare.net/ripplelabs/howard-hinnant-accu2014 slajdy 43 - 53. Podsumowanie: kopiowanie / zamiana to przydatne narzędzie w przyborniku. Ale został on wprowadzony na rynek zbyt często, a następnie był często nadużywany.
Howard Hinnant,
2
@HowardHinnant: Tak, +1 do tego. Napisałem to w czasie, gdy prawie każde pytanie w C ++ brzmiało: „pomóż mojej klasie zawiesić się, gdy ją skopiuję” i to była moja odpowiedź. Jest odpowiedni, gdy chcesz po prostu pracować z semantyką kopiowania / przenoszenia lub cokolwiek, aby przejść do innych rzeczy, ale nie jest to naprawdę optymalne. Jeśli uważasz, że to pomoże, możesz umieścić zastrzeżenie na górze mojej odpowiedzi.
GManNickG

Odpowiedzi:

2183

Przegląd

Dlaczego potrzebujemy idiomu „kopiuj i zamień”?

Każda klasa zarządzająca zasobem ( opakowanie , takie jak inteligentny wskaźnik) musi zaimplementować Wielką Trójkę . Podczas gdy cele i implementacja konstruktora kopii i destruktora są proste, operator przypisywania kopii jest prawdopodobnie najbardziej dopracowany i najtrudniejszy. Jak należy to zrobić? Jakich pułapek należy unikać?

Idiomkopiuj i zamień” jest rozwiązaniem i elegancko pomaga operatorowi przypisania osiągnąć dwie rzeczy: uniknąć duplikacji kodu i zapewnić silną gwarancję wyjątku .

Jak to działa?

Pod względem koncepcyjnym działa przy użyciu funkcji konstruktora kopii, aby utworzyć lokalną kopię danych, a następnie pobiera skopiowane dane za pomocą swapfunkcji, zamieniając stare dane na nowe. Tymczasowa kopia ulega zniszczeniu, zabierając ze sobą stare dane. Pozostaje nam kopia nowych danych.

Aby użyć idiomu kopiowania i zamiany, potrzebujemy trzech rzeczy: działającego konstruktora kopii, działającego niszczyciela (oba są podstawą każdego opakowania, więc i tak powinny być kompletne) oraz swapfunkcji.

Funkcja zamiany to funkcja nie rzucająca, która zamienia dwa obiekty klasy, element członkowski na element członkowski. Możemy ulec pokusie użycia std::swapzamiast dostarczenia własnego, ale byłoby to niemożliwe; std::swapkorzysta z konstruktora kopiowania i operatora przypisania kopii w ramach swojej implementacji, a my ostatecznie staralibyśmy się zdefiniować operatora przypisania sam w sobie!

(Nie tylko to, ale także niewykwalifikowane połączenia z swapnaszym niestandardowym operatorem wymiany, pomijając niepotrzebną konstrukcję i zniszczenie naszej klasy, które std::swapto pociągałoby za sobą).


Dogłębne wyjaśnienie

Cel

Rozważmy konkretny przypadek. Chcemy zarządzać, w skądinąd bezużytecznej klasie, tablicą dynamiczną. Zaczynamy od działającego konstruktora, konstruktora kopii 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 prawie skutecznie zarządza tablicą, ale musi 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; teraz zarządza tablicą, bez wycieków. Ma jednak trzy problemy, oznaczone kolejno w kodzie jako (n).

  1. Pierwszym z nich jest test samodzielnego przypisania. Ta kontrola służy dwóm celom: jest to łatwy sposób, aby uniemożliwić nam uruchamianie niepotrzebnego kodu podczas samodzielnego przypisywania i chroni nas przed subtelnymi błędami (takimi jak usunięcie tablicy tylko w celu jej skopiowania). Ale we wszystkich innych przypadkach służy jedynie spowolnieniu programu i działa jak szum w kodzie; samodzielne przydzielanie rzadko występuje, więc przez większość czasu ta kontrola jest marnotrawstwem. Byłoby lepiej, gdyby operator mógł bez niego prawidłowo działać.

  2. Po drugie, zapewnia jedynie podstawową gwarancję wyjątku. Jeśli się new int[mSize]nie powiedzie, *thiszostanie zmodyfikowany. (Mianowicie, rozmiar jest nieprawidłowy, a danych już nie ma!) Aby uzyskać silną gwarancję wyjątku, musiałoby to być coś w rodzaju:

    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 się rozszerzył! Co prowadzi nas do trzeciego problemu: duplikacji kodu. Nasz operator przypisania skutecznie powiela cały kod, który już napisaliśmy w innym miejscu, i to jest okropne.

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

(Można się zastanawiać: jeśli tak dużo kodu jest potrzebne do prawidłowego zarządzania jednym zasobem, co się stanie, jeśli moja klasa zarządza więcej niż jednym? Chociaż może to wydawać się uzasadnione i rzeczywiście wymaga nie trywialnych try/ catchklauzul, nie jest to -tak. To dlatego, że klasa powinna zarządzać tylko jednym zasobem !)

Udane rozwiązanie

Jak wspomniano, idiom kopiowania i zamiany naprawi wszystkie te problemy. Ale teraz mamy wszystkie wymagania oprócz jednego: swapfunkcji. Chociaż Reguła trzech z powodzeniem pociąga za sobą istnienie naszego konstruktora kopii, operatora przypisania i destruktora, tak naprawdę powinna ona nosić nazwę „Wielka Trójka i Pół”: za każdym razem, gdy klasa zarządza zasobem, sensowne jest również zapewnienie swapfunkcji .

Musimy dodać funkcjonalność 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);
    }

    // ...
};

( Oto wyjaśnienie, dlaczego public friend swap). Teraz możemy nie tylko zamieniać nasze dumb_array, ale ogólnie swapy mogą być bardziej wydajne; po prostu zamienia wskaźniki i rozmiary, a nie alokuje i kopiuje całe tablice. Oprócz tego bonusu w funkcjonalności i wydajności, jesteśmy teraz gotowi do wdrożenia idiomu kopiowania i zamiany.

Bez zbędnych ceregieli nasz operator przypisania to:

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

    return *this;
}

I to wszystko! Za jednym zamachem wszystkie trzy problemy są elegancko rozwiązywane jednocześnie.

Dlaczego to działa?

Najpierw zauważamy ważny wybór: argument parametru jest brany pod uwagę według wartości . Chociaż równie łatwo można wykonać następujące czynności (a nawet wiele naiwnych implementacji tego idiomu):

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

    return *this;
}

Tracimy ważną szansę optymalizacji . Nie tylko to, ale ten wybór jest krytyczny w C ++ 11, który zostanie omówiony później. (Ogólnie rzecz biorąc, niezwykle przydatna wskazówka jest następująca: jeśli masz zamiar zrobić kopię czegoś w funkcji, pozwól kompilatorowi zrobić to na liście parametrów. ‡)

Tak czy inaczej, ta metoda pozyskania naszego zasobu jest kluczem do wyeliminowania powielania kodu: możemy użyć kodu z konstruktora kopii do wykonania kopii i nigdy nie musimy go powtarzać. Po wykonaniu kopii jesteśmy gotowi do wymiany.

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

W tym momencie jesteśmy wolni od domu, ponieważ swapnie rzucamy. Zamieniamy nasze bieżące dane na skopiowane dane, bezpiecznie zmieniając nasz stan, a stare dane są umieszczane w tymczasowym. Stare dane są następnie zwalniane po powrocie funkcji. (Gdzie kończy się zakres parametru, a wywoływany jest jego destruktor.)

Ponieważ idiom nie powtarza kodu, nie możemy wprowadzać błędów w operatorze. Zauważ, że oznacza to, że pozbyliśmy się potrzeby samodzielnego sprawdzania, pozwalającego na jednolitą implementację operator=. (Ponadto nie ponosimy już kary za wydajność w przypadku zadań innych niż samodzielne przydzielanie).

I to jest idiom kopiowania i zamiany.

Co z C ++ 11?

Następna wersja C ++, C ++ 11, wprowadza jedną bardzo ważną zmianę w sposobie zarządzania zasobami: Reguła Trzech jest teraz Regułą Czterech (i pół). Dlaczego? Ponieważ nie tylko musimy być w stanie skopiować-skonstruować nasz zasób, musimy również go przenieść-skonstruować .

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

class dumb_array
{
public:
    // ...

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

    // ...
};

Co tu się dzieje? Przypomnij sobie cel budowy ruchów: pobranie 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ń za pomocą other; wiemy, że domyślnie skonstruowana instancja naszej klasy może być bezpiecznie przypisana i zniszczona, więc wiemy, że otherbędziemy mogli zrobić to samo, po zamianie.

(Należy pamiętać, że niektóre kompilatory nie obsługują delegowania konstruktorów; w tym przypadku musimy ręcznie ręcznie skonstruować klasę. To niefortunne, ale na szczęście trywialne zadanie).

Dlaczego to działa?

To jedyna zmiana, którą musimy wprowadzić w naszej klasie, więc dlaczego to działa? Pamiętaj o bardzo ważnej decyzji, którą podjęliśmy, aby parametr stał się wartością, a nie odniesieniem:

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

Teraz, jeśli otherzostanie zainicjowany za pomocą wartości, zostanie on skonstruowany w ruchu . Doskonały. W ten sam sposób, w C ++ 03, ponownie wykorzystajmy naszą funkcję konstruktora kopii, biorąc argument za wartość, C ++ 11 automatycznie wybierze również konstruktor ruchu, gdy jest to właściwe. (I, oczywiście, jak wspomniano w poprzednio połączonym artykule, kopiowanie / przenoszenie wartości można po prostu całkowicie pominąć).

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


Przypisy

* Dlaczego ustawiamy mArrayna zero? Ponieważ jeśli jakikolwiek dalszy kod w operatorze wyrzuci, dumb_arraymożna wywołać destruktor ; a jeśli tak się stanie bez ustawienia wartości null, próbujemy usunąć pamięć, która została już usunięta! Unikamy tego, ustawiając go na null, ponieważ usunięcie null jest brakiem operacji.

† Istnieją inne twierdzenia, że ​​powinniśmy specjalizować się std::swapw naszym typie, zapewnić w swojej klasie swapbezpłatną funkcję swapitp. Ale to wszystko jest niepotrzebne: każde prawidłowe użycie swapbędzie odbywać się za pośrednictwem niekwalifikowanego połączenia, a nasza funkcja będzie znalezione przez ADL . Jedna funkcja zadziała.

‡ Powód jest prosty: gdy masz zasoby dla siebie, możesz je zamienić i / lub przenieść (C ++ 11) w dowolne miejsce. Wykonując kopię na liście parametrów, maksymalizujesz optymalizację.

†† Konstruktor ruchu powinien zasadniczo być noexcept, w przeciwnym razie część kodu (np. std::vectorLogika zmiany rozmiaru) użyje konstruktora kopiowania, nawet jeśli ruch miałby sens. Oczywiście zaznacz go tylko, jeśli kod w nim nie zgłasza wyjątków.

GManNickG
źródło
17
@GMan: Argumentowałbym, że klasa zarządzająca kilkoma zasobami jednocześnie jest skazana na porażkę (wyjątkowe bezpieczeństwo staje się koszmarne) i zdecydowanie zalecałbym, aby albo klasa zarządzała JEDNYM zasobem LUB miała funkcje biznesowe i używała menedżerów.
Matthieu M.,
22
Nie rozumiem, dlaczego metoda zamiany jest tutaj deklarowana jako przyjaciel?
szx
9
@asd: Aby umożliwić odnalezienie go przez ADL.
GManNickG
8
@neuviemeporte: W nawiasie elementy tablic są domyślnie inicjowane. Bez nich są niezainicjowani. Ponieważ w konstruktorze kopiowania i tak nadpisujemy wartości, możemy pominąć inicjalizację.
GManNickG
10
@neuviemeporte: Musisz swapgo znaleźć podczas ADL, jeśli chcesz, aby działał w najbardziej ogólnym kodzie, na który się natkniesz, jak boost::swapi w innych różnych przykładach wymiany. Zamiana jest trudnym problemem w C ++ i ogólnie wszyscy zgodziliśmy się, że jeden punkt dostępu jest najlepszy (dla zachowania spójności), a jedynym sposobem na zrobienie tego w ogólności jest darmowa funkcja ( intnie może mieć elementu zamiany, na przykład). Zobacz moje pytanie, aby uzyskać trochę tła.
GManNickG
274

U podstaw przypisania znajdują się dwa etapy: zburzenie starego stanu obiektu i zbudowanie nowego stanu jako kopii stanu innego obiektu.

Zasadniczo tak właśnie robią destruktor i konstruktor kopii , więc pierwszym pomysłem byłoby przekazanie im pracy. Ponieważ jednak zniszczenie nie może zawieść, podczas gdy konstrukcja może, chcemy to zrobić odwrotnie : najpierw wykonaj część konstruktywną, a jeśli to się powiedzie, a następnie część destrukcyjną . Idiom „kopiuj i zamień” to sposób, aby to zrobić: najpierw wywołuje konstruktora kopiowania klasy, aby utworzyć obiekt tymczasowy, następnie zamienia dane na dane tymczasowe, a następnie pozwala, aby destruktor tymczasowy zniszczył stary stan.
Odswap()ma nigdy nie zawieść, jedyną częścią, która może zawieść, jest konstrukcja kopii. Jest to wykonywane jako pierwsze, a jeśli się nie powiedzie, nic nie zostanie zmienione w docelowym obiekcie.

W wyrafinowanej formie, kopiowanie i zamiana jest realizowane poprzez wykonanie kopii przez zainicjowanie (nie referencyjnego) parametru operatora przypisania:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
sbi
źródło
1
Myślę, że wspominanie o pimpl jest równie ważne, jak wspominanie o kopii, zamianie i zniszczeniu. Zamiana nie jest magicznie wyjątkowa. Jest bezpieczny, ponieważ zamiana wskaźników jest bezpieczna na wyjątki. Nie musisz używać pimpl, ale jeśli tego nie zrobisz, musisz upewnić się, że każda zamiana członka jest bezpieczna dla wyjątków. To może być koszmar, gdy ci członkowie mogą się zmieniać, a to jest banalne, gdy są ukryte za pimplem. A potem przychodzi koszt pimplu. Co prowadzi nas do wniosku, że często bezpieczeństwo wyjątkowe wiąże się z kosztami wydajności.
wilhelmtell,
7
std::swap(this_string, that)nie zapewnia gwarancji braku rzutu. Zapewnia to wyjątkowe bezpieczeństwo, ale nie gwarantuje braku rzucania.
wilhelmtell,
11
@wilhelmtell: W C ++ 03 nie ma wzmianki o wyjątkach potencjalnie zgłaszanych przez std::string::swap(które są wywoływane przez std::swap). W C ++ 0x std::string::swapjest noexcepti nie może zgłaszać wyjątków.
James McNellis,
2
@sbi @JamesMcNellis jest ok, ale kwestia nadal jest ważna: jeśli masz członków klasy, musisz upewnić się, że ich zamiana jest zabroniona. Jeśli masz pojedynczy element, który jest wskaźnikiem, jest to banalne. W przeciwnym razie tak nie jest.
wilhelmtell,
2
@wilhelmtell: Myślałem, że o to chodzi w zamianie: nigdy się nie rzuca i zawsze jest O (1) (tak, wiem, std::array...)
sbi
44

Istnieje już kilka dobrych odpowiedzi. Skoncentruję się głównie na tym, co moim zdaniem brakuje - objaśnieniu „wad” za pomocą idiomu kopiowania i zamiany…

Co to jest idiom kopiowania i zamiany?

Sposób implementacji operatora przypisania pod względem funkcji zamiany:

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 zasobów potrzebnych do uzyskania nowego stanu (np. pamięć, deskryptory)

  • tego przejęcia można podjąć przed zmodyfikowaniem bieżącego stanu obiektu (tj. *this), jeśli wykonano kopię nowej wartości, dlatego rhsjest akceptowana przez wartość (tj. skopiowana), a nie przez odniesienie

  • zamiana stanu lokalnej kopii rhsi*this zwykle jest względnie łatwa do zrobienia bez potencjalnej awarii / wyjątków, ponieważ lokalna kopia nie potrzebuje później żadnego określonego stanu (po prostu potrzebuje stanu nadającego się do działania destruktora, podobnie jak w przypadku przemieszczanego obiektu od w> = C ++ 11)

Kiedy należy go używać? (Jakie problemy rozwiązuje [/ create] ?)

  • Gdy chcesz, aby przypisane do sprzeciwu nie miało wpływu zadanie przypisujące wyjątek, zakładając, że masz lub możesz napisać swapz silną gwarancją wyjątku, a najlepiej takie, które nie może zawieść / throw.. †

  • Gdy potrzebujesz czystego, łatwego do zrozumienia, solidnego sposobu zdefiniowania operatora przypisania w kategoriach (prostszego) konstruktora kopiowania swapi funkcji destruktora.

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

  • Gdy jakakolwiek utrata wydajności lub chwilowo wyższe zużycie zasobów utworzone przez posiadanie dodatkowego obiektu tymczasowego podczas przypisania nie jest ważne dla Twojej aplikacji. ⁂

swaprzucanie: generalnie możliwe jest niezawodne zamiana elementów danych, które obiekty śledzą za pomocą wskaźnika, ale elementów danych niebędących wskaźnikami, które nie mają zamiany bez rzucania lub dla których zamiana musi zostać zaimplementowana jakoX tmp = lhs; lhs = rhs; rhs = tmp; konstrukcja-kopia lub przypisanie może rzucać, nadal może się nie powieść, pozostawiając niektórych członków danych zamienionych, a innych nie. Ten potencjał dotyczy nawet C ++ 03 std::string, gdy James komentuje inną odpowiedź:

@wilhelmtell: W C ++ 03 nie ma wzmianki o wyjątkach potencjalnie zgłaszanych przez std :: string :: swap (które są wywoływane 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 podczas przypisywania z odrębnego obiektu, może łatwo zawieść w przypadku samodzielnego przypisania. Chociaż może się wydawać niewyobrażalne, że kod klienta próbowałby nawet samodzielnie przypisać, może się to zdarzyć stosunkowo łatwo podczas operacji algo na kontenerach, przy czym x = f(x);kod, gdzie fjest (być może tylko dla niektórych #ifdefgałęzi) makro ala #define f(x) xlub funkcja zwracająca odniesienie x, a nawet (prawdopodobnie nieefektywny, ale zwięzły) kod podobny do 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 przypisywaniu powyższy kod kasujący x.p_;wskazuje p_na nowo przydzielony region sterty, a następnie próbuje odczytać w nim niezainicjowane dane (Niezdefiniowane zachowanie), jeśli nie robi to nic dziwnego,copy próbuje przydzielić się do każdego just- zniszczone „T”!


Id Idiom kopiowania i zamiany może wprowadzać nieefektywności lub ograniczenia z powodu zastosowania dodatkowego tymczasowego (gdy parametr operatora jest kopiowany):

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

Tutaj odręcznie Client::operator=może sprawdzić, czy *thisjest już podłączone do tego samego serwera co rhs(być może wysyłanie kodu „resetującego”, jeśli jest to przydatne), podczas gdy metoda kopiowania i zamiany wywołałaby konstruktora kopiowania, który prawdopodobnie zostałby napisany do otwarcia wyraźne połączenie z gniazdem, a następnie zamknij oryginalne. Może to oznaczać nie tylko zdalną interakcję w sieci zamiast prostej kopii zmiennej procesowej, ale może również ograniczać limity klienta lub serwera dotyczące zasobów gniazd lub połączeń. (Oczywiście ta klasa ma dość okropny interfejs, ale to już inna sprawa ;-P).

Tony Delroy
źródło
4
To powiedziawszy, połączenie przez gniazdo było tylko przykładem - ta sama zasada dotyczy każdej potencjalnie drogiej inicjalizacji, takiej jak sondowanie / inicjalizacja / kalibracja 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) oszustwo. Zgodnie z aktualnymi specyfikacjami technicznie obiekt nie będzie miał operatora przypisania ruchu! Jeśli później zostanie użyty jako członek klasy, nowa klasa nie będzie automatycznie generowana move-ctor! Źródło: youtu.be/mYrbivnruYw?t=43m14s
user362515
3
Główny problem z operatorem przypisania kopii Clientpolega na tym, że przypisanie nie jest zabronione.
sbi
W przykładzie klienta klasa powinna zostać wyłączona.
John Z. Li
25

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

W niektórych wersjach programu Visual Studio (i ewentualnie w innych kompilatorach) występuje błąd, który jest naprawdę denerwujący i nie ma sensu. Więc jeśli zadeklarujesz / zdefiniujesz swoją swapfunkcję w ten sposób:

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

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

}

... kompilator będzie krzyczał na ciebie, gdy wywołasz swapfunkcję:

wprowadź opis zdjęcia tutaj

Ma to coś wspólnego z friendwywoływaną funkcją i thisprzekazywaniem obiektu jako parametru.


Można to obejść bez używania friendsłowa kluczowego i redefinicji swapfunkcji:

void swap(A& other) {

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

}

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

wprowadź opis zdjęcia tutaj


W końcu nie musisz używać friendfunkcji do zamiany 2 obiektów. Równie sensowne jest utworzenie swapfunkcji składowej, która ma jeden otherobiekt jako parametr.

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

Ołeksij
źródło
1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . To jest wersja uproszczona. Wydaje się, że błąd pojawia się za każdym razem, gdy friendfunkcja jest wywoływana z *thisparametrem
Oleksiy
1
@GManNickG, jak powiedziałem, jest to błąd i może działać dobrze dla innych osób. Chciałem tylko pomóc niektórym ludziom, którzy mogą mieć taki sam problem jak ja. Próbowałem tego z Visual Studio 2012 Express i Preview 2013 i jedyną rzeczą, która sprawiła, że ​​zniknął, była moja modyfikacja
Oleksiy
8
@GManNickG nie zmieściłoby się w komentarzu do wszystkich zdjęć i przykładów kodu. I jest w porządku, jeśli ludzie przegłosują, jestem pewien, że jest ktoś, kto ma ten sam błąd; informacje w tym poście mogą być dokładnie tym, czego potrzebują.
Oleksiy
14
zauważ, że to tylko błąd w podświetlaniu kodu IDE (IntelliSense) ... Skompiluje się dobrze bez ostrzeżeń / błędów.
Amro,
3
Zgłoś błąd VS tutaj, jeśli jeszcze tego nie zrobiłeś (i jeśli nie został on naprawiony) connect.microsoft.com/VisualStudio
Matt
15

Chciałbym dodać słowo ostrzeżenia, gdy mamy do czynienia z kontenerami w stylu C ++ 11 obsługującymi alokatory. Zamiana i przypisanie mają subtelnie inną semantykę.

Dla konkretności rozważmy kontener std::vector<T, A>, w którym Ajest jakiś stanowy typ alokatora, i 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 fsi fmjest dać astan, który bmiał początkowo. Istnieje jednak ukryte pytanie: co się stanie, jeśli a.get_allocator() != b.get_allocator()? Odpowiedź brzmi: to zależy. Write Chodźmy AT = std::allocator_traits<A>.

  • Jeśli AT::propagate_on_container_move_assignmenttak std::true_type, to fmponownie przypisuje alokator az wartością b.get_allocator(), w przeciwnym razie nie, i anadal używa swojego pierwotnego alokatora. W takim przypadku elementy danych muszą zostać zamienione indywidualnie, ponieważ przechowywanie ai bniezgodność.

  • Jeśli AT::propagate_on_container_swaptak std::true_type, to fszamienia zarówno dane, jak i alokatory w oczekiwany sposób.

  • Jeśli AT::propagate_on_container_swaptak std::false_type, to potrzebujemy dynamicznej kontroli.

    • Jeśli a.get_allocator() == b.get_allocator(), to dwa pojemniki korzystają z kompatybilnego magazynu, a zamiana przebiega w zwykły sposób.
    • Jeśli jednak a.get_allocator() != b.get_allocator()program zachowuje się w sposób nieokreślony (por. [Container.requirements.general / 8].

Konsekwencją jest to, że zamiana stała się nietrywialną operacją w C ++ 11, gdy tylko kontener zacznie obsługiwać stanowe alokatory. Jest to nieco „zaawansowany przypadek użycia”, ale nie jest to zupełnie mało prawdopodobne, ponieważ optymalizacje przenoszenia zwykle stają się interesujące dopiero, gdy klasa zarządza zasobem, a pamięć jest jednym z najpopularniejszych zasobów.

Kerrek SB
źródło