Što je kopija i swap idiom?

Kakav je to idiom i kada ga treba koristiti? Koje probleme on rješava? Da li se idiom mijenja s C ++ 11?

Iako je to bilo spomenuto na mnogim mjestima, nismo imali nikakvih posebnih pitanja "što je ovo" pitanja i odgovora, pa evo ga. Evo djelomičnog popisa mjesta na kojima je prethodno spomenuto:

1717
19 июля '10 в 11:42 2010-07-19 11:42 GManNickG je postavljen 19. srpnja '10 u 11:42 2010-07-19 11:42
@ 5 odgovora

pregled

Zašto nam je potreban kopija i swap idiom?

Svaka klasa koja upravlja resursom (ljuska, poput inteligentnog pokazivača) mora implementirati Tri Tri . Iako su ciljevi i implementacija konstruktora kopiranja i destruktora jednostavni, operator kopiranja možda je najslabiji i složeniji. Kako to učiniti? Koje zamke izbjegavati?

Kopiranje i swap idiom je rješenje i elegantno pomaže operatoru dodjele u postizanju dvije stvari: izbjegavanje dupliciranja koda i pružanje pouzdanog jamstva izuzetaka .

Kako to funkcionira?

Konceptualno , radi pomoću funkcionalnosti copy-constructor za kreiranje lokalne kopije podataka, zatim preuzima kopirane podatke pomoću funkcije swap , zamjenjujući stare podatke novim podacima. Zatim se privremena kopija uništi, uzimajući stare podatke. Ostavljamo kopiju novih podataka.

Da bismo koristili idiome kopiranja i zamjene, potrebne su nam tri stvari: konstruktor radne instance, radni destruktor (oba su osnova svake ljuske, tako da ih je svejedno potrebno dovršiti) i swap funkciju.

Swap funkcija je ne-metalna funkcija koja zamjenjuje dva objekta klase, član člana. Možda ćemo biti u iskušenju da koristimo std::swap umjesto da nudimo svoje, ali to bi bilo nemoguće; std::swap koristi instancu konstruktora i operatora dodjele kopija u svojoj implementaciji, a mi ćemo na kraju pokušati definirati operatora dodjele u smislu sebe!

(Ne samo to, nego i nekvalificirani swap pozivi koristit će naš prilagođeni swap operator, preskakujući nepotrebne konstrukte i uništavajući našu klasu, što bi značilo std::swap .)


Detaljno objašnjenje

Svrha

Razmotrite poseban slučaj. Želimo kontrolirati inače beskorisnu klasu s dinamičkim nizom. Počnimo s radnim konstruktorom, konstruktorom kopiranja i destruktorom:

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

Ova klasa gotovo uspješno upravlja nizom, ali za ispravan rad zahtijeva operator= .

Loša odluka

Evo kako izgleda naivna implementacija:

 // the hard part dumb_array operator=(const dumb_array other) { if (this !=  // (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 mi kažemo da smo gotovi; sada kontrolira polje bez curenja. Međutim, ona ima tri problema označena sekvencijalno u kodu kao (n) .

  • Prvi je test za navođenje. Ovaj test služi dvije svrhe: to je jednostavan način da nas spriječi u pokretanju nepotrebnog koda za samo-dodjeljivanje i štiti nas od suptilnih pogrešaka (na primjer, brisanje niza samo za kopiranje i kopiranje). Ali u svim drugim slučajevima, on jednostavno usporava program i djeluje kao šum u kodu; samo-studija rijetko se događa, tako da većinu vremena ova provjera je otpad. Bilo bi bolje da operater normalno radi bez njega.

  • Drugo, ono pruža samo osnovno jamstvo iznimke. Ako new int[mSize] ne radi, *this će se promijeniti. (Naime, veličina je pogrešna, a podaci su nestali!) Za pouzdano jamstvo izuzetaka, to bi trebalo biti nešto poput:

     dumb_array operator=(const dumb_array other) { if (this !=  // (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; } 
  • Kôd se proširio! To nas dovodi do trećeg problema: dupliciranje koda. Naš odredišni operator učinkovito kopira sve kodove koje smo napisali negdje drugdje, a to je strašna stvar.

U našem slučaju, njezina se jezgra sastoji od samo dva reda (selekcija i kopiranje), ali s složenijim resursima, ovaj prošireni kod može biti vrlo složen. Moramo nastojati da se ne ponavljamo.

(Možda mislite: ako je ovaj kôd potreban za pravilno upravljanje jednim resursom, što ako moj razred upravlja s više od jednog? Iako se može činiti kao pravi problem, a zapravo zahtijeva ne-trivijalan try / catch , to nije problem. treba upravljati samo jednim resursom !)

Uspješna odluka

Kao što je već spomenuto, kopija i swap idiom će riješiti sve te probleme. Ali sada imamo sve zahtjeve osim jednog: swap . Dok pravilo tri uspješno podrazumijeva postojanje našeg konstruktora kopiranja, operatora dodjele i destruktora, doista bi se trebalo zvati "Big Three i Half": u bilo kojem trenutku vaša klasa kontrolira resurs, također ima smisla dati swap .

Našoj klasi moramo dodati swap funkcionalnost i to učiniti na sljedeći način:

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

( To objašnjava zašto se public friend swap mijenja.) Sada ne možemo samo zamijeniti naš dumb_array , ali swapovi mogu općenito biti učinkovitiji; jednostavno mijenja pokazivače i veličine, umjesto da dodjeljuje i kopira čitave nizove. Uz ovaj bonus u funkcionalnosti i učinkovitosti, sada smo spremni provesti idiom kopiranja i zamjene.

Bez daljeg odlaganja, naša izjava o zadatku je:

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

I ovo! Jednim potezom sva su tri problema odmah riješena.

Zašto to radi?

Prvo, uočavamo važan izbor: argument parametra uzima se po vrijednosti. Iako možete jednako lako učiniti sljedeće (i doista, mnoge naivne implementacije idioma):

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

Gubimo važnu mogućnost optimizacije . I ne samo to, ali ovaj izbor je presudan u C ++ 11, kao što će biti objašnjeno u nastavku. (Općenito, vodič je nevjerojatno koristan: ako ćete učiniti nešto u funkciji, neka kompajler to učini u popisu parametara.

U svakom slučaju, ova metoda dobivanja našeg resursa je ključ za uklanjanje duplikata koda: koristimo kod iz konstruktora kopiranja za stvaranje kopije i nikada ga ne moramo ponavljati. Sada kada je kopija napravljena, spremni smo za razmjenu.

Imajte na umu da kada unesete funkciju, svi novi podaci su već odabrani, kopirani i spremni za korištenje. To je ono što nam daje čvrsto jamstvo za isključivanje besplatno: nećemo ni ući u funkciju ako kopiranje ne uspije, i stoga je nemoguće promijeniti stanje *this . (Ono što smo radili prije, za pouzdano jamstvo iznimke, kompajler sada radi za nas, kao vrsta.)

U ovom trenutku slobodni smo od kuće, jer swap ne odustaje. Sadašnje podatke mijenjamo kopiranim podacima, sigurno mijenjajući naše stanje, a stari podaci padaju u privremene podatke. Tada se stari podaci šalju kada se funkcija vrati. (Gdje se nakon završetka područja parametara i njegovog destruktora naziva.)

Budući da idiom ne ponavlja bilo koji kod, ne možemo unijeti pogreške u operatora. Imajte na umu da to znači da eliminiramo potrebu za provjerom samoodređenja koja omogućuje ujednačenu uniformnu implementaciju operator= . (Osim toga, više nemamo kaznu izvedbe za nepravilne dodjele.)

A to je idiom kopiranja i zamjene.

Što je s C ++ 11?

Sljedeća verzija C ++, C ++ 11, čini jednu vrlo važnu promjenu u načinu upravljanja resursima: sada je pravilo tri sada pravilo četiri (i pol). Zašto? Budući da ne trebamo samo kopirati-graditi naš resurs, moramo ga i premjestiti-izgraditi .

Srećom za nas je lako:

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

Što se ovdje događa? Sjetite se cilja premjestiti-konstrukcije: uzeti resurse iz druge instance klase, ostavljajući je u stanju zajamčenom da se može dodijeliti i uništiti.

Dakle, ono što smo učinili je jednostavno: inicijalizirajte uz pomoć zadanog konstruktora (funkcija C ++ 11), a zatim zamijenite other ; znamo da se zadana instanca naše klase može sigurno dodijeliti i uništiti, tako da znamo da other može učiniti isto nakon što je zamijenjen.

(Imajte na umu da neki kompilatori ne podržavaju delegiranje konstruktora, u tom slučaju moramo ručno stvoriti default klasu. To je nesretan, ali na sreću, trivijalan zadatak.)

Zašto to radi?

To je jedina promjena koju moramo učiniti našoj klasi, pa zašto onda to radi? Zapamtite važnu odluku koju smo napravili kako bismo parametar postavili kao vrijednost, a ne kao referencu:

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

Sada, ako je other inicijaliziran s vrijednošću r, on će biti izgrađen u smjeru kretanja. Izvrsno. Slično tome, C ++ 03 nam omogućuje da ponovno iskoristimo funkcionalnost konstruktora kopiranja, uzimajući argument po vrijednosti, C ++ 11 će automatski odabrati konstruktor pomaka kada je to potrebno. (I, naravno, kao što je spomenuto u ranije povezanom članku, kopiranje / premještanje vrijednosti može biti potpuno eliminirano.)

I tako završava idiom kopiranja i zamjene.


fusnote

Zašto postavljamo mArray na nulu? Budući da, ako je bilo koji dodatni kod u izrazu bačen, može se pozvati destruktor dumb_array ; i ako se to dogodi bez postavljanja vrijednosti na nulu, pokušat ćemo izbrisati već izbrisanu memoriju! To izbjegavamo postavljanjem na nulu, jer uklanjanje null nije operacija.

† Postoje i druge tvrdnje koje bismo trebali specijalizirati std::swap za naš tip, osigurati swap slobodnih funkcija, itd. Unutar klase swap . Ali sve to nije potrebno: svaka ispravna upotreba swap proći će kroz nekvalificirani poziv, a naša će se funkcija naći kroz ADL . Jedna funkcija će učiniti.

• Razlog je jednostavan: ako imate resurs za sebe, možete ga promijeniti i / ili premjestiti (C ++ 11) bilo gdje što bi trebao biti. Čineći kopiju u popisu parametara, maksimizirajte optimizaciju.

1882
19 июля '10 в 11:43 2010-07-19 11:43 odgovor je dao GManNickG 19. srpnja '10 u 11:43 2010-07-19 11:43

Dodjela u srcu sastoji se od dva koraka: razbijanje starog stanja objekta i stvaranje njegovog novog stanja kao kopije drugog stanja objekta.

U osnovi, koji destruktor i konstruktor rade stoga bi prva ideja bila delegirati posao njima. Međutim, budući da razaranje ne bi trebalo propasti, dok gradnja može, doista želimo to učiniti obrnuto: prvo provesti konstruktivni dio , a ako uspije , onda izvršiti destruktivni dio . Kopiraj i zamijeni idiom je način da se napravi upravo to: prvo, on poziva konstruktora instance klase da stvori privremeni, a zatim zamijeni svoje podatke s privremenim, a zatim dopusti privremenom destruktoru da uništi staro stanje.
Budući da swap() nikada ne smije uspjeti, jedini dio koji može uspjeti je kopiranje. To je prvo učinjeno, a ako ne uspije, ništa se neće promijeniti u ciljnom objektu.

border=0

U svom rafiniranom obliku, kopiranje i zamjena provodi se izvođenjem kopije inicijalizacijom (bez reference) parametra operatora dodjele:

 T operator=(T tmp) { this->swap(tmp); return *this; } 
234
19 июля '10 в 11:55 2010-07-19 11:55 odgovor je dan sbi 19. srpnja '10 u 11:55 2010-07-19 11:55

Već imate dobre odgovore. Usredotočit ću se uglavnom na činjenicu da, po mom mišljenju, nisu dovoljno - objašnjenje "minusa" s idiomom "kopiraj i mijenjaj" ....

Što je kopiranje i zamjena idioma?

Način implementacije operatora dodjele u smislu swap funkcije:

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

Osnovna ideja je da:

  • dio zadatka koji najviše podliježe pogreškama je pružiti sve resurse koji su potrebni novom stanju (na primjer, memorija, ručke)

  • tako da možete pokušati pokušati prije promjene trenutnog stanja objekta (tj. *this ), ako je napravljena kopija nove vrijednosti, dakle rhs prihvaća po vrijednosti (tj. kopira se) nego referencom

  • zamjena stanja lokalne kopije rhs i *this obično relativno lako bez potencijalnih grešaka / iznimaka, budući da lokalna kopija ne treba nikakvo posebno stanje (samo zahtijeva stanje pogodno za izvođenje destruktora, kao što je objekt premješten iz> = C ++ 11)

Kada se treba koristiti? (Koje probleme rješava?)

  • Ako želite da određeni objekt nije pogođen zadatkom koji generira iznimku, pod pretpostavkom da imate ili možete napisati swap s pouzdanim jamstvom iznimke i, idealno, onaj koji ne može uspjeti / throw .. †

  • Ako vam je potreban čist, jasan i pouzdan način definiranja operatora dodjele u smislu (jednostavnijih) konstruktora kopiranja, swap i destruktivnih funkcija.

    • Samopridržavanje, koje se izvodi kao "kopiranje i zamjena", omogućuje vam izbjegavanje uobičajenih slučajeva.

  • Ako bilo koje ograničenje izvedbe ili kratkoročno korištenje resursa, stvoreno s dodatnim privremenim objektom u vrijeme dodjele, nije važno za vašu prijavu. ⁂

† bacanje svopova: u pravilu je moguće pouzdano zamijeniti elemente podataka, koji objekat prate po pokazivaču, ali ne i indikativne elemente podataka koji nemaju swap-swap ili za koje se moraju mijenjati X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; i kopiranje ili dodjela može biti bačena, još uvijek postoji mogućnost neuspjeha ako se neki članovi podataka zamijene, a drugi nisu. Ovaj potencijal vrijedi čak i za C ++ 03 std::string , jer James komentira drugi odgovor:

@ wilhelmtell: Ne spominje se iznimke u C ++ 03, koje se mogu odabrati korištenjem std :: string :: swap (koji se zove std :: swap). U C ++ 0x, std :: string :: swap noexcept i ne smije generirati iznimke. - James McNellis 22. prosinca 2010. u 15:24


- implementacija operatora dodjele, koji se čini razumnim kada se dodjeljuje od pojedinačnog objekta, može lako propasti za samoodređenje. Iako se može činiti nezamislivim da kod klijenta čak pokušava izvršiti samoodređenje, to se može dogoditi relativno lako tijekom operacija algo u spremnicima s kodom x = f(x); gdje f (možda samo za neke grane #ifdef ) ala makro #define f(x) x ili funkcija koja vraća referencu na x ili čak (vjerojatno nedjelotvoran ali kratak) kod, na primjer x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). Na primjer:

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

Kod samoodređenja, gornji kod uklanja x.p_; , p_ ukazuje na novo dodijeljeno područje hrpe, zatim pokušava pročitati neinicijalizirane podatke u njemu (Undefined Behavior), ako to ne čini ništa čudno, copy pokušava izvesti samo-ime za svaku novo uništenu "T"!


M Idiom "kopiraj i zamijeni" može dovesti do neučinkovitosti ili ograničenja zbog korištenja dodatnog vremena (kada je operator-operator konstruiran iz sadržaja):

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

Ovdje rukom pisani Client::operator= može provjeriti da je *this već povezano s istim poslužiteljem kao rhs (možda šalje "reset" kod, ako je koristan), dok će se pristup kopiranja i promjene odnositi na instancu konstruktora, koji će najvjerojatnije biti napisan da bi se otvorila zasebna utičnica, a zatim zatvorila izvorna. To može značiti ne samo daljinsku mrežnu interakciju, nego i jednostavno kopiranje procesne varijable u procesu rada, može biti u suprotnosti s ograničenjima klijenta ili poslužitelja na resurse ili priključke utičnice. (Naravno, ova klasa ima prilično grozno sučelje, ali to je drugo pitanje; -P).

33
06 марта '14 в 17:51 2014-03-06 17:51 Odgovor dao Tony Delroy 6. ožujka 2014. u 17:51 2014-03-06 17:51

Taj je odgovor više kao dodavanje i malo izmjena gore navedenih odgovora.

Neke verzije programa Visual Studio (a možda i druge kompilacije) imaju pogrešku koja je doista neugodna i besmislena. Stoga, ako deklarirate / definirate svoju swap funkciju kako slijedi:

 friend void swap(A first, A second) { std::swap(first.size, second.size); std::swap(first.arr, second.arr); } 

... kompajler će vrištati na vas kada pozovete swap funkciju:

2019

20
04 сент. Odgovor je dan Oleksiy 04 sep . 2013-09-04 07:50 '13 u 7:50 2013-09-04 07:50