Czym jest idiom kopiowanie i wymiana?
Czym jest ten idiom i kiedy należy go używać? Jakie problemy rozwiązuje? Czy idiom zmienia się po użyciu C++11?
Chociaż było to wspominane w wielu miejscach, nie mieliśmy żadnego pojedynczego pytania" co to jest " i odpowiedzi, więc oto jest. Oto częściowa lista miejsc, w których był wcześniej wymieniony:
5 answers
Przegląd
Dlaczego potrzebujemy idiomu copy-and-swap?
Każda klasa, która zarządza zasobem (opakowanie , jak inteligentny wskaźnik), musi zaimplementować wielką trójkę . Podczas gdy cele i implementacja konstruktora kopiującego i destruktora są proste, operator kopiującego przypisania jest prawdopodobnie najbardziej dopracowany i trudny. Jak należy to zrobić? Jakich pułapek należy unikać?
Idiom copy-and-swap jest rozwiązanie i elegancko pomaga operatorowi przypisania osiągnąć dwie rzeczy: unikanie powielania kodu i zapewnienie silnej gwarancji wyjątku .
Jak to działa?
Koncepcyjnie , działa poprzez użycie funkcji copy-constructor do utworzenia lokalnej kopii danych, a następnie pobiera skopiowane dane za pomocą funkcji swap
, zamieniając stare dane na nowe. Następnie Kopia tymczasowa ulega zniszczeniu, zabierając ze sobą stare dane. Jesteśmy w lewo z kopią nowych danych.
Aby użyć idiomu copy-and-swap, potrzebujemy trzech rzeczy: działającego konstruktora kopiującego, działającego destruktora (oba są podstawą każdego wrappera, więc i tak powinny być kompletne) i swap
funkcji.
Funkcja swap jest nie rzucającą funkcją, która zamienia dwa obiekty klasy, member na member. Możemy pokusić się o użycie std::swap
zamiast podania własnego, ale byłoby to niemożliwe; std::swap
używa konstruktora kopiującego i operatora przyporządkowania kopii w ramach jego implementacji, a ostatecznie będziemy próbować zdefiniować operatora przyporządkowania pod względem samym w sobie!
(nie tylko to, ale niewykwalifikowane wywołania do swap
użyją naszego niestandardowego operatora swap, pomijając niepotrzebną budowę i zniszczenie naszej klasy, które pociągnęłyby za sobą std::swap
.)
Szczegółowe wyjaśnienie
Cel
Rozważmy konkretny przypadek. Chcemy zarządzać, w zupełnie bezużytecznej klasie, a tablica dynamiczna. Zaczynamy od działającego konstruktora, konstruktora kopiującego 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 zarządza tablicą z powodzeniem, ale musi operator=
działać poprawnie.
Nieudane rozwiązanie
Tak 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 skończyliśmy; to teraz zarządza tablicą, bez przecieków. Jednak cierpi na trzy problemy, oznaczone kolejno w kodzie jako (n)
.
Pierwszy to test na samodzielne zadanie. To sprawdzanie 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 po to, aby spróbować ją skopiować). Ale we wszystkich innych przypadkach służy jedynie spowolnieniu programu i działa jak szum w kodzie; samo-przypisanie rzadko się zdarza, więc przez większość czasu ta kontrola jest marnotrawstwem. Byłoby lepiej, gdyby operator mógł pracować prawidłowo bez niego.
-
Drugi jest to, że zapewnia tylko podstawową gwarancję wyjątku. Jeśli
new int[mSize]
nie powiedzie się,*this
zostanie zmodyfikowany. (Mianowicie rozmiar jest zły, a dane zniknęły! W związku z tym, że nie jest to możliwe, nie jest to możliwe.]}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; }
Kod się rozszerzył! Co prowadzi nas do trzeciego problemu: powielanie kodu. Nasz operator przydziałów skutecznie powiela cały kod, który już napisaliśmy gdzie indziej, a to straszna rzecz.
W naszym przypadku, rdzeń to tylko dwie linie (alokacja i kopia), ale przy bardziej złożonych zasobach ten kod może być dość kłopotliwy. Powinniśmy starać się nigdy się nie powtarzać.
(można by się zastanawiać: jeśli ta ilość kodu jest potrzebna do prawidłowego zarządzania jednym zasobem, co jeśli moja klasa zarządza więcej niż jednym? Chociaż może się to wydawać słusznym problemem, a w rzeczywistości wymaga nietrywialnego try
/catch
klauzule, to nie jest problem. To dlatego, że klasa powinna zarządzać jeden zasób tylko!)
Skuteczne rozwiązanie
Jak wspomniano, idiom Kopiuj i wymieniaj rozwiąże wszystkie te problemy. Ale w tej chwili mamy wszystkie wymagania oprócz jednego: funkcję swap
. Podczas gdy reguła trzech z powodzeniem pociąga za sobą istnienie naszego konstruktora kopiującego, operatora przypisania i destruktora, tak naprawdę powinna być nazywana "wielką trójką i pół": za każdym razem, gdy twoja klasa zarządza zasobem, ma również sens, aby zapewnić swap
funkcję.
Musimy dodać Zamiana funkcjonalności na naszą klasę, A 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);
}
// ...
};
Bez dalszych ceregieli, naszym operatorem przypisania jest:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
I to jest to! Jednym zamachem wszystkie trzy problemy są elegancko rozwiązywane naraz.
Dlaczego to działa?
Najpierw zauważamy ważny wybór: argument parametru jest brany przez-wartość. Chociaż można równie łatwo zrobić następujące (i rzeczywiście, wiele naiwnych implementacji idiomu zrobić): {]}
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
Tracimy ważną możliwość optymalizacji . Nie tylko to, ale ten wybór jest krytyczny w C++11, który jest omówiony później. (Na ogólne Uwaga, 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 pozyskiwania naszego zasobu jest kluczem do wyeliminowania duplikacji kodu: możemy użyć kodu z konstruktora kopiującego, aby wykonać kopię i nigdy nie musimy powtarzać żadnego bitu. Teraz, gdy kopia została wykonana, jesteśmy gotowi do wymiany.
Zauważ, że po wprowadzeniu funkcji wszystkie nowe dane są już przydzielone, skopiowane i gotowe do użycia. To daje nam silną gwarancję WYJĄTKÓW za darmo: nie wejdziemy nawet do funkcji, jeśli konstrukcja kopii zawiedzie, i dlatego nie jest możliwe, aby zmienić stan *this
. (Co zrobiliśmy ręcznie wcześniej dla silnej gwarancji WYJĄTKÓW, kompilator robi dla nas teraz; jak miło.)
W tym momencie jesteśmy wolni od domu, ponieważ swap
nie rzucamy. Zamieniamy nasze bieżące dane z kopiowanymi, bezpiecznie zmieniając nasz stan, a stare dane zostają wprowadzone do tymczasowego. Stare dane są następnie zwalniane, gdy funkcja powróci. (Gdzie na końcu zakresu parametru i wywołany jest jego Destruktor.)
Ponieważ idiom nie powtarza kodu, nie możemy wprowadzać błędów wewnątrz operatora. Zauważ, że oznacza to, że jesteśmy pozbawieni potrzeby samodzielnego sprawdzenia, pozwalając na jednolitą implementację operator=
. (Dodatkowo, nie mamy już kary za wykonywanie zadań bez siebie.)
I to jest idiom copy-and-swap.
A co z C++11?
Kolejna wersja C++, C++11, wprowadza jedną bardzo ważną zmianę w sposobie zarządzania zasobami: reguła trzech jest terazregułą czterech (i pół). Dlaczego? Ponieważ nie tylko musimy być w stanie kopiować-konstruować nasz zasób, musimy również go przenosić-konstruować.
Na szczęście dla nas, to proste:
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? Przypomnijmy sobie cel ruchu-Budowa: aby wziąć zasoby z innej instancji klasy, pozostawiając ją w stanie gwarantowanym do przypisania i zniszczenia.
Więc to, co zrobiliśmy jest proste: zainicjalizować za pomocą domyślnego konstruktora (funkcja C++11), a następnie swap z other
; wiemy, że domyślna instancja naszej klasy może być bezpiecznie przypisana i zniszczona, więc wiemy, że {27]} będzie w stanie zrobić to samo, po zamianie.
(zauważ, że niektóre Kompilatory nie obsługują delegowania konstruktorów; w tym przypadku musimy ręcznie domyślnie konstruuj klasę. Jest to niefortunne, ale na szczęście trywialne zadanie.)
Dlaczego to działa?
To jedyna zmiana, jaką musimy wprowadzić w naszej klasie, więc dlaczego to działa? Zapamiętaj zawsze ważną decyzję, jaką podjęliśmy, aby parametr był wartością, a nie referencją:dumb_array& operator=(dumb_array other); // (1)
Teraz, jeśli other
jest inicjalizowana z wartością R, będzie skonstruowana z ruchu . Idealnie. W ten sam sposób C++03 pozwala nam ponownie wykorzystać funkcjonalność copy-constructor C++11 automatycznie wybierze konstruktor move-constructor, jeśli jest to właściwe. (I, oczywiście, jak wspomniano wcześniej w linkowanym artykule, kopiowanie / przenoszenie wartości może zostać całkowicie pominięte.)
I tak kończy się idiom copy-and-swap.
Przypisy
*dlaczego ustawiamy mArray
NA null? Ponieważ jeśli jakiś kolejny kod w operatorze rzuca, może zostać wywołany Destruktor dumb_array
; a jeśli tak się stanie bez ustawiając ją na null, próbujemy usunąć pamięć, która została już usunięta! Unikamy tego, ustawiając go na null, ponieważ usunięcie null nie jest operacją.
†istnieją inne twierdzenia, że powinniśmy specjalizować się std::swap
dla naszego typu, dostarczać w klasie swap
wzdłuż boku funkcji swobodnej swap
itd. Ale to wszystko jest niepotrzebne: jakiekolwiek właściwe użycie swap
będzie przez wywołanie bez zastrzeżeń, a nasza funkcja zostanie znaleziona przez ADL . Wystarczy jedna funkcja.
‡powodem jest proste: gdy masz zasób dla siebie, możesz go zamienić i / lub przenieść (C++11) w dowolne miejsce. Wykonując kopię na liście parametrów, maksymalizujesz optymalizację.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:54
Zadanie, w jego sercu, to dwa kroki: zburzenie starego stanu obiektu oraz budowanie nowego państwa jako kopia stanu jakiegoś innego obiektu.
Zasadniczo, to jest to, co destructor oraz Konstruktor kopiujący zrobić, więc pierwszym pomysłem byłoby delegowanie pracy do nich. Jednakże, ponieważ zniszczenie nie może zawieść, podczas gdy budowa może, w rzeczywistości chcemy zrobić to na odwrót : najpierw wykonaj część konstruktywną a jeśli to się uda, więc zrób niszczycielską część. Idiom copy-and-swap jest na to sposobem: najpierw wywołuje Konstruktor kopiujący klasy, aby utworzyć tymczasowy, a następnie zamienia jego dane na tymczasowe, a następnie pozwala destruktorowi tymczasowemu zniszczyć stary stan.
Ponieważ swap()
ma nigdy nie zawieść, jedyną częścią, która może zawieść, jest konstrukcja kopiująca. To jest wykonywane najpierw, a jeśli się nie powiedzie, nic nie zostanie zmienione w docelowym obiekcie.
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-22 14:26:50
Jest już kilka dobrych odpowiedzi. Skupię się głównie na tym, co myślę, że im brakuje-wyjaśnieniu "minusów" z idiomem copy-and-swap....
Czym jest idiom copy-and-swap?
Sposób implementacji operatora przyporządkowania pod względem funkcji swap:
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, których nowy stan potrzebuje nabyte (np. pamięć, deskryptory)
Ta akwizycja może być podjęta przed modyfikacją bieżącego stanu obiektu (tj.
*this
), jeśli zostanie wykonana kopia nowej wartości, dlategorhs
jest akceptowana przez wartość (tj. skopiowana), a nie przez odniesienieZmiana stanu lokalnej kopii
rhs
i*this
jest zazwyczaj stosunkowo łatwa do zrobienia bez potencjalnych błędów / WYJĄTKÓW, biorąc pod uwagę, że lokalna kopia nie po uruchomieniu destruktora potrzebny jest jakiś konkretny stan, podobnie jak obiekt przeniesiony Z in > = C++11)
Kiedy należy go stosować? (Jakie problemy rozwiązuje [/create] ?)
Jeśli chcesz, aby przypisane-to nie miało wpływu na przypisanie, które rzuca wyjątek, zakładając, że masz lub możesz napisać
swap
z silną gwarancją wyjątku, a najlepiej taki, który nie może fail/throw
..†-
Gdy chcesz mieć przejrzysty, łatwy do zrozumienia, solidny sposób definiowania operatora przypisania w kategoriach (prostszego) konstruktora kopiującego,
swap
i funkcji destruktora.- SELF-assignment done as a copy-and-swap pozwala uniknąć często pomijanych przypadków edge.‡
- gdy jakakolwiek kara wydajnościowa lub chwilowo wyższe wykorzystanie zasobów utworzone przez posiadanie dodatkowego tymczasowego obiektu podczas przydziału nie jest ważne dla Ciebie podanie. ⁂
† swap
rzucanie: na ogół możliwe jest niezawodne Wymienianie elementów danych, które obiekty śledzą za pomocą wskaźnika, ale elementy danych nie-wskaźnikowych, które nie mają wymiany bez rzucania, lub dla których Zamiana musi być zaimplementowana jako X tmp = lhs; lhs = rhs; rhs = tmp;
i konstrukcja kopiowania lub przypisanie mogą rzucać, nadal mogą nie działać, pozostawiając niektóre elementy danych zamienione, a inne nie. Ten potencjał odnosi się nawet do C++03 std::string
's jak James komentuje inną odpowiedź:
@wilhelmtell: w C++03 nie ma wzmianki o wyjątkach potencjalnie rzucanych przez std:: string:: swap (które jest wywoływane przez std:: swap). W C++0x, std:: string:: swap jest noexcept i nie może wyrzucać WYJĄTKÓW. - James McNellis Dec 22 ' 10 at 15: 24
‡ implementacja operatora przyporządkowania, która wydaje się rozsądna przy przypisywaniu z odrębnego obiektu, może łatwo zawieść dla przypisania własnego. Chociaż może się wydawać niewyobrażalne, że kod klienta spróbuje nawet sam się przydzielić, może można to zrobić stosunkowo łatwo podczas operacji algo na kontenerach, z kodem x = f(x);
gdzie f
jest (być może tylko dla niektórych gałęzi #ifdef
) makro ala #define f(x) x
lub funkcją zwracającą odwołanie do x
, a nawet (prawdopodobnie nieefektywnym, ale zwięzłym) kodem 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 delete x.p_;
wskazuje p_
na nowo przydzielony region sterty, a następnie próbuje odczytać niezainicjalizowane dane w nim zawarte (nieokreślone zachowanie), jeśli nie robi nic dziwnego, copy
próbuje przypisać się do każdego właśnie zniszczonego "T"!
Idiom copy-and-swap może wprowadzać nieefektywności lub ograniczenia ze względu na użycie dodatkowej tymczasowej (gdy parametr operatora jest konstruowany jako kopia):
struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
};
Tutaj, ręcznie napisany Client::operator=
Może Sprawdzić, czy *this
jest już podłączony do tego samego serwera co rhs
(Być może wysyłanie kodu "reset", jeśli jest to użyteczne), podczas gdy podejście copy-and-swap wywoła Konstruktor kopiujący, który prawdopodobnie zostałby napisany w celu otwarcia odrębnego połączenia gniazda, a następnie zamknięcia oryginalnego. Nie tylko może to oznaczać zdalną interakcję z siecią zamiast prostej kopii zmiennych w procesie, ale może również powodować ograniczenia klientów lub serwerów w zasobach gniazd lub połączeniach. (Oczywiście ta klasa ma dość okropny interfejs, ale to już inna sprawa ; - P).
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-07 06:46:09
Ta odpowiedź jest bardziej jak dodatek i niewielka modyfikacja do powyższych odpowiedzi.
W niektórych wersjach Visual Studio (i ewentualnie innych kompilatorów) jest błąd, który jest naprawdę irytujący i nie ma sensu. Jeśli więc zadeklarujesz / zdefiniujesz swoją swap
funkcję 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 funkcję swap
:
Ma to coś wspólnego z wywołaniem friend
funkcji i this
obiektu przekazany jako parametr.
Sposobem obejścia tego problemu jest nie używanie słowa kluczowego friend
i ponowne zdefiniowanie funkcji swap
:
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
Tym razem możesz po prostu wywołać swap
i przekazać other
, czyniąc kompilator szczęśliwym:
W końcu nie musisz używać funkcji friend
do zamiany 2 obiektów. Równie sensowne jest stworzenie swap
funkcji member, która ma jeden obiekt other
jako parametr.
Masz już dostęp do this
obiekt, więc podanie go jako parametru jest technicznie zbędne.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-04 05:34:49
Chciałbym dodać słowo ostrzeżenia, gdy masz do czynienia z kontenerami świadomymi alokatorów w stylu C++11. Zamiana i przypisanie mają subtelnie różną semantykę.
Dla konkretnej wartości rozważmy kontener std::vector<T, A>
, Gdzie A
jest pewnym stateful allocator type, A 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 nadanie a
stanu, 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>
.
Jeśli
AT::propagate_on_container_move_assignment
jeststd::true_type
, tofm
przypisuje alokatora
z wartościąb.get_allocator()
, w przeciwnym razie nie ia
nadal używa swojego pierwotnego alokatora. W takim przypadku elementy danych muszą być wymieniane indywidualnie, ponieważ przechowywaniea
ib
nie jest zgodne.Jeśli
AT::propagate_on_container_swap
jeststd::true_type
, tofs
zamienia zarówno dane, jak i alokatory w oczekiwanym Moda.-
Jeśli
AT::propagate_on_container_swap
jeststd::false_type
, to potrzebujemy dynamicznego sprawdzenia.- jeśli
a.get_allocator() == b.get_allocator()
, to oba pojemniki używają kompatybilnego magazynu, a Zamiana odbywa się w zwykły sposób. - Jednakże, jeśli
a.get_allocator() != b.get_allocator()
, program ma nieokreślone zachowanie (por. [Pojemnik.wymagania.ogólne / 8].
- jeśli
Wynik jest taki, że zamiana stała się nietrywialną operacją w C++11, gdy tylko kontener zacznie obsługiwać alokatory stateful. Jest to nieco "zaawansowany przypadek użycia", ale nie jest to całkowicie mało prawdopodobne, ponieważ optymalizacje ruchu zwykle stają się interesujące dopiero wtedy, gdy Klasa zarządza zasobem, a pamięć jest jednym z najpopularniejszych zasobów.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-24 08:16:06