Kas yra kopija ir apsikeitimo idioma?

Kas yra ši idioma ir kada ji turėtų būti naudojama? Kokias problemas jis sprendžia? Ar idioma keičiasi su C + + 11?

Nors tai buvo paminėta daugelyje vietų, mes neturėjome jokių specialių klausimų ir atsakymų, todėl čia. Čia pateikiamas dalinis vietų, kuriose jis anksčiau buvo paminėtas, sąrašas:

1717
19 июля '10 в 11:42 2010-07-19 11:42 GManNickG yra nustatytas liepos 19 d. 10 val. 11:42 2010-07-19 11:42
@ 5 atsakymai

Peržiūra

Kodėl mums reikia kopijuoti ir keisti idiomą?

Bet kuri klasė, kuri valdo išteklius (apvalkalą, kaip ir protingas žymeklis), turi įgyvendinti tris tris . Nors kopijavimo konstruktoriaus ir destruktoriaus tikslai ir įgyvendinimas yra paprasti, kopijavimo operatorius galbūt yra labiausiai niuansuotas ir sudėtingiausias. Kaip tai padaryti? Kokių sunkumų reikia vengti?

Kopijavimas ir apsikeitimo idiomas yra sprendimas ir elegantiškai padeda priskyrimo operatoriui pasiekti du dalykus: išvengti kodų dubliavimo ir patikimos išimties garantijos .

Kaip tai veikia?

Konceptualiai ji veikia naudodama kopijavimo konstruktoriaus funkciją, kad sukurtų vietinę duomenų kopiją, o po to perkelia kopijuotus duomenis naudodama swap funkciją, pakeisdama senus duomenis naujais duomenimis. Tuomet laikina kopija sunaikinama, su ja sunaikindami senus duomenis. Mes paliekame naujų duomenų kopiją.

Norint naudoti kopijavimo ir keitimo idiomas, mums reikia trijų dalykų: darbo instancijos konstruktoriaus, veikiančio naikintojo (abu yra bet kokio korpuso pagrindas, todėl jie turi būti baigti) ir swap funkcija.

Apsikeitimo funkcija yra ne metalizuojanti funkcija, kuri apsikeičia dviem klasės objektais, nario nariu. Mes galime būti linkę naudoti std::swap o ne suteikti savo, bet tai būtų neįmanoma; std::swap naudoja konstruktoriaus egzempliorių ir kopijavimo priskyrimo operatorių, o galiausiai bandysime apibrėžti priskyrimo operatorių pagal save!

(Ne tik tai, bet ir nekvalifikuoti swap skambučiai naudos mūsų pasirinktinį apsikeitimo operatorių, praleisdami nereikalingas konstrukcijas ir sunaikindami mūsų klasę, kuri reikštų std::swap .)


Išsamus paaiškinimas

Tikslas

Apsvarstykite konkretų atvejį. Mes norime valdyti kitaip nenaudingą klasę dinamišku masyvu. Pradėkime nuo darbo konstruktoriaus, kopijavimo konstruktoriaus ir destruktoriaus:

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

Ši klasė beveik sėkmingai valdo masyvą, tačiau teisingam darbui reikia operator= .

Blogas sprendimas

Štai ką gali atrodyti naivus įgyvendinimas:

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

Ir mes sakome, kad esame padaryti; dabar ji valdo masyvą be nutekėjimo. Tačiau jis kenčia nuo trijų problemų, kurios eilės tvarka yra pažymėtos kode kaip (n) .

  • Pirmasis yra išbandymas. Šis patikrinimas atliekamas dviem tikslams: tai yra paprastas būdas užkirsti kelią nereikalingam kodui savarankiškai priskirti ir apsaugoti mus nuo subtilių klaidų (pavyzdžiui, ištrinti masyvą tik kopijuoti ir kopijuoti). Tačiau visais kitais atvejais tai tiesiog lėtina programą ir veikia kaip triukšmas kode; savarankiškas darbas retai vyksta, todėl didžiąją laiko dalį šis patikrinimas yra atliekos. Būtų geriau, jei operatorius galėtų dirbti be jo.

  • Antra, ji suteikia tik pagrindinę išimties garantiją. Jei new int[mSize] neveikia, *this bus pakeista. (Būtent, dydis yra neteisingas, o duomenys yra prarasti!) Dėl patikimos išimties garantijos tai turėtų būti:

     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; } 
  • Šis kodas išplėstas! Tai veda prie trečios problemos: kodavimo dubliavimo. Mūsų paskirties operatorius veiksmingai dublikuoja visą kodą, kurį parašėme kitur, ir tai yra baisus dalykas.

Mūsų atveju jos pagrindą sudaro tik dvi eilutės (atranka ir kopijavimas), tačiau sudėtingesniais ištekliais šis ištinęs kodas gali būti gana sudėtingas. Mes turime stengtis niekada nesikartoti.

(Galbūt manote: jei šis kodas reikalingas tinkamai valdyti vieną šaltinį, kas, jei mano klasė valdytų daugiau nei vieną? Nors tai gali atrodyti kaip reali problema, ir iš tikrųjų tam reikia ne trivialinio try / catch , tai nėra problema. turėtų valdyti tik vieną šaltinį !)

Sėkmingas sprendimas

Kaip jau minėta, kopijavimo ir apsikeitimo idioma nustatys visas šias problemas. Bet dabar turime visus reikalavimus, išskyrus vieną: swap . Nors trijų taisyklių taisyklė sėkmingai lemia mūsų kopijavimo konstruktoriaus, priskyrimo operatoriaus ir destruktoriaus egzistavimą, ji tikrai turėtų būti vadinama „Big Three“ ir „Half“: bet kuriuo metu jūsų klasė valdo išteklį, taip pat prasminga teikti swap .

Turime pridėti swap funkcionalumą mūsų klasėje, ir mes tai darome taip:

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

( Tai paaiškina, kodėl public friend swap .) Dabar mes galime ne tik keistis mūsų dumb_array , bet apsikeitimo sandoriai paprastai gali būti efektyvesni; ji paprasčiausiai keičia rodykles ir dydžius, o ne paskirsto ir kopijuoja visas masyvus. Be šios funkcionalumo ir efektyvumo premijos, dabar esame pasirengę įgyvendinti kopijavimo ir keitimo idiomą.

Be tolesnių veiksmų, mūsų priskyrimo pareiškimas yra:

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

Ir tai! Vienu smūgiu, visos trys problemos yra elegantiškai išspręstos nedelsiant.

Kodėl tai veikia?

Pirma, pastebime svarbų pasirinkimą: parametro argumentas priimamas pagal vertę. Nors galite taip pat lengvai atlikti šiuos veiksmus (ir iš tiesų, daugybę naivių idiomų įgyvendinimų):

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

Mes prarandame svarbią optimizavimo galimybę . Ne tik tai, bet šis pasirinkimas yra labai svarbus C ++ 11, kaip bus aptarta toliau. (Apskritai, vadovas yra nepaprastai naudingas: jei ketinate ką nors atlikti funkcijoje, leiskite kompiliatoriui tai padaryti parametrų sąraše.) ‡)

Bet kokiu atveju šis mūsų išteklių gavimo būdas yra raktas į kodo dubliavimą: mes naudojame kodą iš kopijavimo konstruktoriaus, kad galėtume sukurti kopiją ir niekada to nereikia pakartoti. Dabar, kai kopija yra padaryta, mes pasiruošę keistis.

Atkreipkite dėmesį, kad įvedus funkciją, visi nauji duomenys jau yra pasirinkti, nukopijuoti ir paruošti naudoti. Tai suteikia mums tvirtą garantiją dėl išimties nemokamai: mes net neįvyksime į funkciją, jei kopija bus nesėkminga, todėl neįmanoma pakeisti šios būsenos. (Ką mes darėme rankiniu būdu, siekiant užtikrinti patikimą išimties garantiją, kompiliatorius dabar mums, kaip natūra, daro tai.)

Šiuo metu mes esame laisvi nuo namų, nes swap neperduoda. Mes keičiame esamus duomenis kopijuotais duomenimis, saugiai keičiame savo būseną, o seni duomenys suskirstomi į laikinus duomenis. Tada senieji duomenys išvedami, kai funkcija grąžinama. (Kur po parametro srities pabaigos ir jo destruktoriaus pavadinimo.)

Kadangi idiomas nekartoja kodo, operatoriuje negalime įvesti klaidų. Atkreipkite dėmesį, kad tai reiškia, kad mes pašaliname poreikį atlikti savikontrolės patikrinimą, kuris leidžia vienodai įgyvendinti vienodą operator= . (Be to, nebegalime taikyti našumo bausmės už netinkamus paskyrimus.)

Ir tai yra kopijavimo ir keitimo idėja.

Ką apie C ++ 11?

Kita C ++, C ++ 11 versija daro labai svarbų pakeitimą, kaip valdome išteklius: dabar trečioji taisyklė dabar yra keturios taisyklės (ir pusė). Kodėl? Kadangi mums reikia ne tik nukopijuoti mūsų išteklius, bet ir turime ją perkelti .

Laimei mums tai lengva:

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

Kas čia vyksta? Prisiminkite perkėlimo-statybos tikslą: paimti išteklius iš kitos klasės egzemplioriaus, paliekant ją valstybėje, kuri garantuojama būti perduodama ir sunaikinama.

Taigi, tai, ką mes padarėme, yra paprasta: inicijuokite numatytąjį konstruktorių (funkcija C ++ 11), tada pakeiskite other ; mes žinome, kad numatytąjį mūsų klasės egzempliorių galima saugiai priskirti ir sunaikinti, todėl žinome, kad other pakeisdamas gali padaryti tą patį.

(Atkreipkite dėmesį, kad kai kurie kompiliatoriai nepalaiko konstruktoriaus delegacijos, tokiu atveju privalome rankiniu būdu sukurti numatytąją klasę. Tai yra nelaimingas, bet, laimei, trivialus uždavinys.)

Kodėl tai veikia?

Tai yra vienintelis pasikeitimas, kurį turime padaryti mūsų klasėje, tad kodėl jis veikia? Prisiminkite svarbų sprendimą, kurį atlikome, kad parametras būtų reikšmė, o ne nuoroda:

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

Dabar, jei other inicijuojamas reikšme r, jis bus pastatytas važiavimo kryptimi. Puikus Panašiai, C ++ 03 leidžia mums pakartotinai panaudoti savo funkciją kopijavimo konstruktoriui, atsižvelgiant į argumentą pagal vertę, C ++ 11 automatiškai pasirinks judėjimo konstruktorių, kai reikia. (Ir, žinoma, kaip minėta anksčiau susietame straipsnyje, kopijavimas / perkėlimas gali būti visiškai pašalintas.)

Ir taip baigiasi kopijavimo ir keitimo idioma.


Išnašos

Kodėl mes nustatome „ mArray nulį? Kadangi, jei pareiškime yra išmestas papildomas kodas, gali būti vadinamas destruktorius dumb_array ; ir jei taip atsitinka nenustatant jo vertės iki nulio, bandysime ištrinti jau ištrintą atmintį! Tai vengiame nustatydami nulį, nes nulio pašalinimas nėra operacija.

† Yra ir kitų teiginių, kad mes turime specializuotis std::swap mūsų tipui, suteikti nemokamą funkcijų swap ir pan. Tačiau visa tai nėra būtina: bet koks tinkamas swap bus atliekamas nekvalifikuotu skambučiu, o mūsų funkcija bus aptikta per ADL . Viena funkcija bus.

Reason Priežastis yra paprasta: jei turite išteklių sau, galite jį pakeisti ir (arba) perkelti (C ++ 11) bet kurioje vietoje. Ir atlikdami kopiją parametrų sąraše, maksimaliai optimizuojate.

1882 m
19 июля '10 в 11:43 2010-07-19 11:43 atsakymą pateikė GManNickG liepos 19 d. 10 val. 11:43 2010-07-19 11:43

Užduotis širdyje susideda iš dviejų etapų: sunaikinti senąją objekto būseną ir sukurti naują būseną kaip kitos objekto būsenos kopiją .

Iš esmės, ką daro destruktorius ir konstruktorius todėl pirmoji idėja būtų perduoti darbą jiems. Tačiau, kadangi sunaikinimas neturėtų žlugti, o statyba gali, mes iš tikrųjų norime tai daryti atvirkščiai: pirmiausia atlikite konstruktyviąją dalį ir, jei pavyksta , atlikite destruktyviąją dalį . Kopijavimo ir apsikeitimo idiomas yra būdas tai padaryti: pirma, jis kviečia klasės instancijos konstruktorių sukurti laikinąjį, tada apsikeičia savo duomenimis su laikinuoju, o tada leidžia laikinam sunaikintuvui sunaikinti senąją valstybę.
Kadangi swap() niekada nepavyks, vienintelė nepavykusioji dalis yra kopijavimas. Tai daroma pirmiausia, o jei ji nepavyksta, tiksliniame objekte nieko nebus pakeista.

border=0

Savo rafinuotu būdu kopijavimas ir apsikeitimas yra įgyvendinami atliekant kopiją inicijuojant (be nuorodos) priskyrimo operatoriaus parametrą:

 T operator=(T tmp) { this->swap(tmp); return *this; } 
234
19 июля '10 в 11:55 2010-07-19 11:55 atsakymas pateikiamas sbi liepos 19 d., 10 val. 11:55 2010-07-19 11:55

Turite gerų atsakymų. Daugiausia sutelksiu dėmesį į tai, kad, mano nuomone, jų nepakanka - „minusų“ paaiškinimas su idioma „kopijuoti ir apsikeisti“.

Kas yra kopijuoti ir pakeisti idiomą?

Būdas įgyvendinti priskyrimo operatorių pagal apsikeitimo funkciją:

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

Pagrindinė idėja yra tokia:

  • labiausiai klaidinga užduoties dalis objektui yra suteikti visus naujai būsenai reikalingus išteklius (pvz., atminties, rankenos)

  • kad galėtumėte pabandyti prieš keičiant dabartinę objekto būseną (t. y. *this ), jei buvo sukurta naujos vertės kopija, todėl rhs priimamas pagal vertę (ty kopijuojamas), nei pagal nuorodą

  • vietinės rhs ir *this kopijos būsenos rhs paprastai *this gana lengva padaryti be potencialių nesėkmių / išimčių, nes vietinei kopijai nereikia jokios konkrečios būsenos (tiesiog reikia, kad sunaikintojui būtų tinkama, kaip objektas, perkeliamas iš> = C ++ 11)

Kada jis turėtų būti naudojamas? (Kokias problemas ji sprendžia [/ create] ?)

  • Jei norite, kad paskirtas objektas nepaveiktų užduoties, kuri sukuria išimtį, darant prielaidą, kad turite arba galite parašyti swap su patikima išimties garantija ir, idealiu atveju, tokiu, kuris negali sugesti / throw .. †

  • Jei jums reikia švaraus, aiškaus ir patikimo būdo apibrėžti priskyrimo operatorių pagal (paprastesnę) kopijavimo konstruktorių, swap ir destruktyvias funkcijas.

    • Savęs paskyrimas, atliekamas kaip „kopijavimas ir apsikeitimas“, leidžia išvengti įprastų atvejų.

  • Jei jūsų paraiškai nėra svarbus bet koks našumo apribojimas arba trumpalaikis išteklių naudojimas, sukurtas naudojant papildomą laikiną objektą paskyrimo metu. ⁂

swap : paprastai galima patikimai pakeisti duomenų elementus, kurie žymi takelį pagal rodyklę, bet ne orientacinius duomenų elementus, kurie neturi apsikeitimo apsikeitimo sandorio arba kurių atveju reikia keistis X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; kopijavimas ar priskyrimas gali būti išmestas, vis tiek yra nesėkmių tikimybė, jei kai kurie duomenų nariai bus pakeisti ir kiti nėra. Šis potencialas taikomas net ir C ++ 03 std::string , nes James komentuoja kitą atsakymą:

@wilhelmtell: C ++ 03 nėra jokių išimčių, kurias galima pasirinkti naudojant std :: string :: swap (kuris vadinamas std :: swap). C ++ 0x, std :: string :: swap noexcept ir neturėtų generuoti išimčių. - James McNellis 2010 m. Gruodžio 22 d. 15:24 val


Ignment priskyrimo operatoriaus, kuris atrodo priimtinas, priskirdamas iš individualaus objekto, įgyvendinimas gali lengvai nepavykti apsispręsti. Nors gali atrodyti neįsivaizduojama, kad kliento kodas netgi bando atlikti apsisprendimą, tai gali įvykti gana lengvai per algo operacijas talpyklose su kodu x = f(x); kur f (galbūt tik kai kurioms šakoms #ifdef ) ala makro #ifdef #define f(x) x arba funkcija, kuri grąžina nuorodą į x arba net (tikriausiai neveiksmingą, bet trumpą) kodą, pavyzdžiui, x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). Pavyzdžiui:

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

Savęs apsisprendimu, aukščiau nurodytas kodas pašalina x.p_; , p_ nurodo naujai paskirto krūvos ploto, tada bando skaityti neracionalizuotus duomenis (neapibrėžtas elgesys), jei tai nieko nedaro, copy bando atlikti savęs pavadinimą kiekvienam naujai sunaikintam „T“!


⁂ Idiomas „kopijavimas ir apsikeitimas“ gali sukelti neveiksmingumą ar apribojimus dėl papildomo laiko naudojimo (kai operatorius yra sudarytas iš turinio):

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

Čia rankiniu būdu sudarytas Client::operator= gali patikrinti, ar *this jau *this prijungtas prie to paties serverio kaip ir rhs (galbūt siunčiant „iš naujo“ kodą, jei jis yra naudingas), o kopijavimo ir keitimo metodas bus susijęs su konstruktoriaus pavyzdžiu, kuri greičiausiai bus parašyta atidaryti atskirą lizdo ryšį ir tada uždarykite originalų. Tai gali reikšti ne tik nuotolinio tinklo sąveiką, bet ir paprastą procesų kintamojo kopijavimą darbo procese, ji gali prieštarauti kliento ar serverio apribojimams, susijusiems su ištekliais ar lizdo jungtimis. (Žinoma, ši klasė turi gana baisią sąsają, bet tai dar vienas klausimas; -P).

33
06 марта '14 в 17:51 2014-03-06 17:51 Atsakymas, kurį pateikė Tony Delroy Kovas 06 '14, 17:51 2014-03-06 17:51

Šis atsakymas yra labiau panašus į pirmiau pateiktų atsakymų pridėjimą ir šiek tiek pakeičiant.

Kai kurios „Visual Studio“ (ir galbūt kitų kompiliatorių) versijos turi klaidą, kuri yra tikrai erzina ir beprasmiška. Todėl, jei deklaruojate / apibrėžiate savo swap funkciją:

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

... kompiliatorius rėkia jums, kai skambinsite swap funkcija:

2019

20
04 сент. Atsakymas pateikiamas Oleksiy 04 sep . 2013-09-04 07:50 '13, 7:50, 2013-09-04 07:50

Norėčiau pridėti įspėjimo žodį, kai dirbate su konteineriais, kurie palaiko C ++ 11 tipo konteinerius. Raginimas ir priskyrimas turi subtiliai skirtingą semantiką.

Для конкретности рассмотрим контейнер std::vector<T, A> , где A - некоторый тип распределения с использованием состояний, и мы сравним следующие функции:

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

Цель обеих функций fs и fm состоит в том, чтобы дать A состояние, в котором b было первоначально. Однако есть скрытый вопрос: что произойдет, если a.get_allocator() != b.get_allocator() ? Ответ: Это зависит. Пусть написано AT = std::allocator_traits<A> .

  • Если AT::propagate_on_container_move_assignment - std::true_type , то fm переназначает распределитель A значением b.get_allocator() , иначе это не так, и A продолжает использовать свой исходный распределитель. В этом случае элементы данных необходимо поменять отдельно, поскольку хранилище A и b несовместимо.

  • Если AT::propagate_on_container_swap - std::true_type , тогда fs заменяет как данные, так и распределители ожидаемым образом.

  • Если AT::propagate_on_container_swap - std::false_type , нам нужна динамическая проверка.

    • Если a.get_allocator() == b.get_allocator() , то два контейнера используют совместимое хранилище, а замена происходит обычным способом.
    • Однако, если a.get_allocator() != b.get_allocator() , программа имеет поведение undefined (см. [container.requirements.general/8].