Apa itu idiom copy and swap?

Apa idiom ini dan kapan harus digunakan? Masalah apa yang dia pecahkan? Apakah idiom berubah dengan C ++ 11?

Meskipun disebutkan di banyak tempat, kami tidak memiliki pertanyaan dan jawaban "apa ini" khusus, jadi ini dia. Berikut adalah sebagian daftar tempat yang sebelumnya disebutkan:

1717
19 июля '10 в 11:42 2010-07-19 11:42 GManNickG diatur pada 19 Juli '10 pada 11:42 2010-07-19 11:42
@ 5 balasan

Ulasan

Mengapa kita membutuhkan idiom copy-and-swap?

Setiap kelas yang mengelola sumber daya (shell, seperti pointer cerdas) harus menerapkan Tiga Tiga . Meskipun tujuan dan implementasi copy constructor dan destructor sederhana, operator penugasan copy mungkin yang paling bernuansa dan kompleks. Bagaimana cara melakukannya? Perangkap apa yang harus dihindari?

Copy and swap idiom adalah solusi dan membantu operator tugas secara elegan dalam mencapai dua hal: menghindari duplikasi kode dan memberikan jaminan pengecualian yang dapat diandalkan .

Bagaimana cara kerjanya?

Secara konseptual , ini bekerja menggunakan fungsionalitas copy-constructor untuk membuat salinan data lokal, kemudian mengambil data yang disalin menggunakan fungsi swap , menggantikan data lama dengan data baru. Kemudian salinan sementara dimusnahkan, dengan membawa data lama. Kami meninggalkan salinan data baru.

Untuk menggunakan idiom menyalin dan bertukar, kita memerlukan tiga hal: konstruktor instance kerja, destruktor kerja (keduanya adalah dasar dari shell, jadi mereka harus diselesaikan pula) dan fungsi swap .

Fungsi swap adalah fungsi non-metaling yang menukar dua objek kelas, anggota untuk anggota. Kita mungkin tergoda untuk menggunakan std::swap alih-alih menyediakan milik kita sendiri, tetapi itu tidak mungkin; std::swap menggunakan instance konstruktor dan operator penugasan salinan dalam implementasinya, dan kami akhirnya akan mencoba untuk menentukan operator penugasan dalam hal diri kami sendiri!

(Tidak hanya ini, tetapi juga panggilan swap tidak memenuhi syarat akan menggunakan operator swap kustom kami, melewatkan konstruksi yang tidak perlu dan menghancurkan kelas kami, yang akan memerlukan std::swap .)


Penjelasan terperinci

Tujuan dari

Pertimbangkan kasus tertentu. Kami ingin mengontrol kelas yang tidak berguna dengan array dinamis. Mari kita mulai dengan konstruktor yang berfungsi, salin konstruktor dan destruktor:

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

Kelas ini hampir berhasil mengelola array, tetapi untuk operasi yang benar diperlukan operator= .

Keputusan yang buruk

Berikut ini terlihat seperti apa implementasi yang naif:

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

Dan kita katakan kita sudah selesai; sekarang mengontrol array tanpa kebocoran. Namun, itu menderita dari tiga masalah berlabel berurutan dalam kode sebagai (n) .

  • Yang pertama adalah tes homing. Pemeriksaan ini memiliki dua tujuan: ini adalah cara mudah untuk mencegah kami menjalankan kode yang tidak perlu untuk penugasan sendiri dan melindungi kami dari kesalahan kecil (misalnya, menghapus array hanya untuk menyalin dan menyalin). Tetapi dalam semua kasus lain, itu hanya memperlambat program dan bertindak sebagai noise dalam kode; belajar mandiri jarang terjadi, sehingga sebagian besar waktu pemeriksaan ini sia-sia. Akan lebih baik jika operator bisa bekerja normal tanpa dia.

  • Kedua, hanya memberikan jaminan pengecualian dasar. Jika new int[mSize] tidak berfungsi, *this akan diubah. (Yaitu, ukurannya salah, dan datanya hi>

     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; } 
  • Kode telah diperluas! Ini membawa kita ke masalah ketiga: duplikasi kode. Operator tujuan kami secara efektif menduplikasi semua kode yang telah kami tulis di tempat lain, dan ini adalah hal yang mengerikan.

Dalam kasus kami, intinya hanya terdiri dari dua baris (seleksi dan salin), tetapi dengan sumber daya yang lebih kompleks, kode kembung ini bisa sangat kompleks. Kita harus berusaha untuk tidak mengu>

(Anda mungkin berpikir: jika kode ini diperlukan untuk mengelola satu sumber daya dengan benar, bagaimana jika kelas saya mengelola lebih dari satu? Meskipun mungkin tampak seperti masalah nyata, dan pada kenyataannya memerlukan upaya / catch non-sepele, ini bukan masalah. Ini karena harus mengelola hanya satu sumber daya !)

Keputusan yang berhasil

Seperti yang telah disebutkan, idiom copy dan swap akan memperbaiki semua masalah ini. Tetapi saat ini kami memiliki semua persyaratan kecuali satu: swap . Sementara aturan tiga berhasil mensyaratkan keberadaan copy constructor, operator penugasan, dan destruktor kami, itu benar-benar harus disebut "Tiga Besar dan Setengah": kapan saja kelas Anda mengontrol sumber daya, juga masuk akal untuk memberikan swap .

Kami perlu menambahkan fungsionalitas swap ke kelas kami, dan kami melakukannya sebagai berikut:

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

( Ini menjelaskan mengapa public friend swap .) Sekarang kita tidak hanya dapat menukar dumb_array kita, tetapi swap umumnya dapat lebih efisien; itu hanya mengubah pointer dan ukuran, daripada mengalokasikan dan menyalin seluruh array. Selain bonus ini dalam fungsionalitas dan efisiensi, kami sekarang siap menerapkan idiom menyalin dan bertukar.

Tanpa basa-basi lagi, pernyataan tugas kami adalah:

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

Dan ini! Dengan satu pukulan, ketiga masalah diselesaikan dengan elegan segera.

Mengapa ini bekerja?

Pertama, kami melihat pilihan penting: argumen parameter diambil berdasarkan nilai. Meskipun Anda dapat dengan mudah melakukan hal berikut (dan memang, banyak implementasi idiom yang naif):

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

Kami kehi>peluang optimalisasi yang penting . Tidak hanya itu, tetapi pilihan ini sangat penting dalam C ++ 11, seperti yang akan dibahas di bawah ini. (Secara umum, panduan ini sangat berguna: jika Anda akan melakukan sesuatu dalam suatu fungsi, biarkan kompiler melakukannya dalam daftar parameter. ‡)

Bagaimanapun, metode mendapatkan sumber daya ini adalah kunci untuk menghi>

Harap dicatat bahwa ketika Anda memasukkan suatu fungsi, semua data baru sudah dipilih, disalin, dan siap digunakan. Inilah yang memberi kami jaminan pengecualian yang kuat secara gratis: kami bahkan tidak akan masuk fungsi jika konstruksi salinan gagal, dan karenanya tidak mungkin mengubah keadaan *this . (Apa yang kami lakukan dengan tangan sebelumnya, untuk jaminan pengecualian yang dapat diandalkan, kompiler melakukannya untuk kami sekarang, sebagai jenis.)

Saat ini kami bebas dari rumah, karena swap tidak menyerah. Kami menukar data kami saat ini dengan data yang disalin, dengan aman mengubah keadaan kami, dan data lama masuk ke data sementara. Kemudian data lama adalah output ketika fungsi kembali. (Di mana setelah akhir area parameter dan destruktornya disebut.)

Karena idiom tidak mengu>operator= . (Selain itu, kami tidak lagi memiliki penalti kinerja untuk penugasan yang tidak patut.)

Dan ini adalah ungkapan menyalin dan menukar.

Bagaimana dengan C ++ 11?

Versi C ++ selanjutnya, C ++ 11, membuat satu perubahan yang sangat penting dalam cara kita mengelola sumber daya: sekarang aturan tiga sekarang adalah Aturan Empat (dan setengah). Mengapa Karena kita tidak hanya perlu menyalin-membangun sumber daya kita , kita juga perlu memindahkan-membangunnya .

Untungnya bagi kita itu mudah:

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

Apa yang sedang terjadi di sini? Ingat tujuan konstruksi-bergerak: untuk mengambil sumber daya dari instance kelas lain, membiarkannya dalam kondisi yang dijamin dapat dialihkan dan dapat dirusak.

Jadi, apa yang telah kita lakukan adalah sederhana: inisialisasi dengan bantuan konstruktor default (fungsi C ++ 11), lalu ganti other ; kami tahu bahwa instance default dari kelas kami dapat ditetapkan dan dimusnahkan dengan aman, jadi kami tahu bahwa yang other dapat melakukan hal yang sama setelah diganti.

(Perhatikan bahwa beberapa kompiler tidak mendukung delegasi konstruktor, dalam hal ini kita harus secara manual membuat kelas default. Ini adalah tugas yang sial, tetapi untungnya, sepele.)

Mengapa ini bekerja?

Ini adalah satu-satunya perubahan yang perlu kita lakukan untuk kelas kita, jadi mengapa itu berhasil? Ingat keputusan penting yang kami buat untuk membuat parameter menjadi nilai, bukan referensi:

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

Sekarang, jika yang other diinisialisasi dengan nilai r, itu akan dibangun ke arah perjalanan. Bagus Demikian pula, C ++ 03 memungkinkan kita untuk menggunakan kembali fungsionalitas kita untuk copy constructor, dengan mengambil argumen berdasarkan nilai, C ++ 11 akan secara otomatis memilih move constructor bila perlu. (Dan, tentu saja, sebagaimana disebutkan dalam artikel yang ditautkan sebelumnya, menyalin / memindahkan nilai dapat dihi>

Dan berakhirlah ungkapan menyalin dan bertukar.


Catatan kaki

Mengapa kita mengatur mArray ke nol? Karena, jika ada kode tambahan dalam pernyataan yang dilemparkan, destructor dumb_array dapat dipanggil; dan jika ini terjadi tanpa menetapkan nilainya ke nol, kami akan mencoba menghapus memori yang sudah dihapus! Kami menghindari ini dengan menetapkannya ke nol, karena menghapus nol bukanlah operasi.

† Ada pernyataan lain bahwa kita harus mengkhususkan std::swap untuk tipe kita, menyediakan swap fungsi gratis, dll. Di dalam swap kelas. Tetapi semua ini tidak perlu: penggunaan swap akan melalui panggilan yang tidak memenuhi syarat, dan fungsi kami akan ditemukan melalui ADL . Satu fungsi akan dilakukan.

‡ Alasannya sederhana: jika Anda memiliki sumber daya sendiri, Anda dapat mengubahnya dan / atau memindahkan (C ++ 11) di tempat mana pun seharusnya. Dan dengan membuat salinan dalam daftar parameter, Anda memaksimalkan optimasi.

1882
19 июля '10 в 11:43 2010-07-19 11:43 jawabannya diberikan oleh GManNickG 19 Juli '10 di 11:43 2010-07-19 11:43

Penugasan dalam hati terdiri dari dua >mengganggu keadaan lama objek dan , menciptakan keadaan baru sebagai salinan keadaan lain dari objek.

Pada dasarnya, apa yang destruktor dan konstruktor lakukan karena itu ide pertama adalah untuk mendelegasikan pekerjaan kepada mereka. Namun, karena kerusakan seharusnya tidak gagal, sementara konstruksi dapat, kami benar-benar ingin melakukannya sebaliknya: pertama melaksanakan bagian yang konstruktif , dan jika berhasil , maka lakukan bagian yang merusak . Idi copy-and-swap adalah cara untuk melakukan hal itu: pertama, ia memanggil konstruktor instance kelas untuk membuat yang sementara, kemudian menukar datanya dengan yang sementara, dan kemudian memungkinkan destruktor sementara untuk menghancurkan keadaan lama.
Karena swap() seharusnya tidak pernah gagal, satu-satunya bagian yang bisa gagal adalah menyalin. Ini dilakukan terlebih dahulu, dan jika gagal, tidak ada yang akan berubah di objek target.

Dalam bentuknya yang disempurnakan, penyalinan dan swap diimplementasikan dengan melakukan salinan dengan menginisialisasi (tanpa referensi) parameter dari operator penugasan:

 T operator=(T tmp) { this->swap(tmp); return *this; } 
234
19 июля '10 в 11:55 2010-07-19 11:55 jawabannya diberikan sbi 19 Juli, '10 jam 11:55 2010-07-19 11:55

Sudah punya jawaban bagus. Saya akan fokus terutama pada kenyataan bahwa, menurut pendapat saya, mereka tidak cukup - penjelasan tentang "minus" dengan idiom "copy and swap" ....

Apa itu salin dan ganti idiom?

Cara menerapkan operator penugasan dalam hal fungsi swap:

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

Ide dasarnya adalah:

  • bagian paling rawan kesalahan dari penugasan ke objek adalah menyediakan sumber daya apa pun yang diperlukan oleh negara baru (misalnya, memori, pegangan)

  • sehingga Anda dapat mencoba untuk mencoba sebelum mengubah keadaan objek saat ini (yaitu *this ), jika salinan nilai baru telah dibuat, oleh karena itu rhs diterima oleh nilai (yaitu, disalin) daripada dengan referensi

  • mengganti keadaan salinan lokal rhs dan *this biasanya relatif mudah dilakukan tanpa potensi kegagalan / pengecualian, karena salinan lokal tidak memerlukan keadaan tertentu (hanya memerlukan keadaan yang sesuai untuk dijalankan oleh destruktor, seperti objek yang dipindahkan dari> = C ++ 11)

Kapan itu harus digunakan? (Masalah apa yang dipecahkan [/ buat] ?)

  • Jika Anda ingin objek yang ditunjuk tidak terpengaruh oleh tugas yang menghasilkan pengecualian, dengan asumsi Anda memiliki atau dapat menulis swap dengan jaminan pengecualian yang dapat diandalkan dan, idealnya, objek yang tidak dapat gagal / throw .. †

  • Jika Anda memerlukan cara yang bersih, jelas, dan dapat diandalkan untuk mendefinisikan operator penugasan dalam hal konstruktor penyalinan ( swap dan destruktif.

    • Penugasan sendiri, dilakukan sebagai "salin dan tukar", memungkinkan Anda untuk menghindari kasus-kasus umum.

  • Jika ada batasan kinerja atau penggunaan sumber daya jangka pendek, yang dibuat dengan objek sementara tambahan pada saat penugasan, tidak penting untuk aplikasi Anda. ⁂

swap throwing: sebagai aturan, dimungkinkan untuk mengganti elemen data dengan andal, yang melacak objek dengan pointer, tetapi bukan elemen data indikatif yang tidak memiliki swap-swap atau yang X tmp = lhs; lhs = rhs; rhs = tmp; harus ditukar X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; dan copy-build atau penugasan mungkin dilemparkan, masih ada kemungkinan gagal jika beberapa anggota data ditukar dan yang lainnya tidak. Potensi ini berlaku bahkan untuk std::string C ++ 03 std::string , seperti komentar James pada jawaban lain:

@wilhelmtell: Tidak disebutkan pengecualian di C ++ 03, yang dapat dipilih menggunakan std :: string :: swap (yang disebut dengan std :: swap). Dalam C ++ 0x, std :: string :: swap noexcept dan seharusnya tidak menghasilkan pengecualian. - James McNellis 22 Des 2010 jam 3:24 siang


‡ implementasi operator penugasan, yang tampaknya masuk akal ketika menugaskan dari objek individu, dapat dengan mudah gagal untuk penentuan nasib sendiri. Meskipun mungkin tampak tidak terbayangkan bahwa kode klien bahkan mencoba melakukan penentuan nasib sendiri, itu dapat terjadi relatif mudah selama operasi algo dalam wadah dengan kode x = f(x); di mana f (mungkin hanya untuk beberapa cabang #ifdef ) secara makro #define f(x) x atau fungsi yang mengembalikan referensi ke x atau bahkan (mungkin tidak efektif tapi singkat) kode, misalnya x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). Sebagai contoh:

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

Dalam penentuan nasib sendiri, kode di atas menghapus x.p_; , p_ menunjuk ke area tumpukan yang baru dialokasikan, kemudian mencoba membaca data yang tidak diinisialisasi di dalamnya (Perilaku Tidak Terdefinisi), jika ini tidak melakukan sesuatu yang aneh, copy mencoba melakukan nama diri untuk setiap "T" yang baru dihancurkan!


Idi Ungkapan "salin dan tukar" dapat menyebabkan inefisiensi atau pembatasan karena penggunaan waktu tambahan (ketika operator-operator dibuat dari konten):

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

Di sini, Client::operator= tulisan tangan Client::operator= dapat memeriksa bahwa *this sudah terhubung ke server yang sama dengan rhs (mungkin mengirim kode "reset" jika berguna), sedangkan pendekatan salin dan ubah akan merujuk ke instance konstruktor, yang kemungkinan besar akan ditulis untuk membuka koneksi soket yang terpisah, dan kemudian menutup yang asli. Ini dapat berarti tidak hanya interaksi jaringan jarak jauh, tetapi juga penyalinan sederhana dari variabel proses dalam proses kerja, itu dapat bertentangan dengan pembatasan klien atau server pada sumber daya atau koneksi soket. (Tentu saja, kelas ini memiliki antarmuka yang agak mengerikan, tapi itu masalah lain; -P).

33
06 марта '14 в 17:51 2014-03-06 17:51 Balas diberikan oleh Tony Delroy 06 Maret 14 di 17:51 2014-03-06 17:51

Jawaban ini lebih seperti menambahkan dan sedikit memodifikasi jawaban di atas.

Beberapa versi Visual Studio (dan mungkin kompiler lain) memiliki kesalahan yang benar-benar menjengkelkan dan tidak berarti. Karena itu, jika Anda mendeklarasikan / menetapkan fungsi swap Anda sebagai berikut:

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

... kompiler akan berteriak kepada Anda ketika Anda memanggil fungsi swap :

2019

20
04 сент. Jawabannya diberikan Oleksiy 04 sep . 2013-09-04 07:50 '13 pada 7:50 2013-09-04 07:50