Apa idiom ini dan kapan harus digunakan? Masalah apa yang dipecahkan? Apakah idiom berubah ketika C ++ 11 digunakan?
Meskipun telah disebutkan di banyak tempat, kami tidak memiliki pertanyaan dan jawaban "apa itu", jadi ini dia. Berikut adalah sebagian daftar tempat yang sebelumnya disebutkan:
Jawaban:
Gambaran
Mengapa kita membutuhkan idiom copy-and-swap?
Setiap kelas yang mengelola sumber daya ( pembungkus , seperti penunjuk pintar) perlu menerapkan Tiga Besar . Sementara tujuan dan implementasi copy-constructor dan destructor sangat mudah, operator copy-assign bisa dibilang yang paling bernuansa dan sulit. Bagaimana seharusnya itu dilakukan? Perangkap apa yang perlu dihindari?
The copy-dan-swap idiom adalah solusi, dan elegan membantu operator penugasan dalam mencapai dua hal: menghindari duplikasi kode , dan memberikan jaminan pengecualian yang kuat .
Bagaimana cara kerjanya?
Secara konseptual , ia bekerja dengan menggunakan fungsionalitas copy-constructor untuk membuat salinan data lokal, kemudian mengambil data yang disalin dengan suatu
swap
fungsi, menukar data lama dengan data baru. Salinan sementara kemudian rusak, dengan membawa data lama. Kami memiliki salinan data baru.Untuk menggunakan idiom copy-and-swap, kita memerlukan tiga hal: copy-constructor yang berfungsi, sebuah destructor yang berfungsi (keduanya adalah dasar dari pembungkus apa pun, jadi bagaimanapun juga harus lengkap), dan sebuah
swap
fungsi.Fungsi swap adalah fungsi non-melempar yang menukar dua objek kelas, anggota untuk anggota. Kita mungkin tergoda untuk menggunakan
std::swap
alih-alih menyediakan milik kita sendiri, tetapi ini tidak mungkin;std::swap
menggunakan copy-constructor dan operator copy-assignment dalam implementasinya, dan kami pada akhirnya akan mencoba mendefinisikan operator assignment dalam hal itu sendiri!(Tidak hanya itu, tetapi panggilan tidak memenuhi syarat
swap
akan menggunakan operator swap kustom kami, melompati konstruksi yang tidak perlu dan penghancuran kelas kami yangstd::swap
akan memerlukan.)Penjelasan mendalam
Hasil
Mari kita pertimbangkan kasus nyata. Kami ingin mengelola, dalam kelas yang tidak berguna, array dinamis. Kita mulai dengan konstruktor yang berfungsi, copy-konstruktor, dan destruktor:
Kelas ini hampir berhasil mengatur array, tetapi perlu
operator=
bekerja dengan benar.Solusi yang gagal
Begini tampilan implementasi yang naif:
Dan kita katakan kita sudah selesai; ini sekarang mengelola sebuah array, tanpa kebocoran. Namun, ia menderita tiga masalah, yang ditandai secara berurutan dalam kode sebagai
(n)
.Yang pertama adalah tes penugasan diri. Pemeriksaan ini memiliki dua tujuan: ini adalah cara mudah untuk mencegah kami menjalankan kode yang tidak perlu pada penugasan sendiri, dan ini melindungi kami dari bug halus (seperti menghapus array hanya untuk mencoba dan menyalinnya). Tetapi dalam semua kasus lain, ini hanya berfungsi untuk memperlambat program, dan bertindak sebagai noise dalam kode; penugasan diri jarang terjadi, sehingga sebagian besar waktu pemeriksaan ini sia-sia. Akan lebih baik jika operator dapat bekerja dengan baik tanpa itu.
Yang kedua adalah bahwa itu hanya memberikan jaminan pengecualian dasar. Jika
new int[mSize]
gagal,*this
pasti sudah dimodifikasi. (Yaitu, ukurannya salah dan datanya hilang!) Untuk jaminan pengecualian yang kuat, itu harus berupa sesuatu yang mirip dengan:Kode telah diperluas! Yang membawa kita ke masalah ketiga: duplikasi kode. Operator penugasan kami secara efektif menduplikasi semua kode yang telah kami tulis di tempat lain, dan itu adalah hal yang mengerikan.
Dalam kasus kami, intinya hanya dua baris (alokasi dan salinan), tetapi dengan sumber daya yang lebih kompleks, kode ini bisa sangat merepotkan. Kita harus berusaha untuk tidak pernah mengulangi diri kita sendiri.
(Orang mungkin bertanya-tanya: jika kode sebanyak ini diperlukan untuk mengelola satu sumber daya dengan benar, bagaimana jika kelas saya mengelola lebih dari satu? Meskipun ini tampaknya menjadi masalah yang valid, dan memang itu membutuhkan non-sepele
try
/catch
klausa, ini bukan -issue. Itu karena kelas harus mengelola satu sumber daya saja !)Solusi yang sukses
Seperti disebutkan, idiom copy-and-swap akan memperbaiki semua masalah ini. Tetapi saat ini, kami memiliki semua persyaratan kecuali satu:
swap
fungsi. Sementara Aturan Tiga berhasil mensyaratkan keberadaan copy-constructor, operator penugasan, dan destruktor kami, itu harus benar-benar disebut "Tiga Besar dan Setengah": setiap kali kelas Anda mengelola sumber daya, masuk akal juga untuk menyediakanswap
fungsi .Kami perlu menambahkan fungsionalitas swap ke kelas kami, dan kami melakukannya sebagai berikut †:
( Inilah penjelasan mengapa
public friend swap
.) Sekarang kita tidak hanya dapat menukar milik kitadumb_array
, tetapi swap secara umum dapat lebih efisien; itu hanya menukar pointer dan ukuran, daripada mengalokasikan dan menyalin seluruh array. Selain dari bonus ini dalam fungsi dan efisiensi, kami sekarang siap untuk mengimplementasikan idiom copy-and-swap.Tanpa basa-basi lagi, operator penugasan kami adalah:
Dan itu dia! Dengan satu gerakan, ketiga masalah diselesaikan secara elegan sekaligus.
Mengapa ini berhasil?
Pertama-tama kita perhatikan pilihan penting: argumen parameter diambil menurut nilai . Sementara orang bisa dengan mudah melakukan hal berikut (dan memang, banyak implementasi naif dari idiom lakukan):
Kami kehilangan peluang optimalisasi yang penting . Tidak hanya itu, tetapi pilihan ini sangat penting dalam C ++ 11, yang akan dibahas kemudian. (Pada catatan umum, pedoman yang sangat berguna adalah sebagai berikut: jika Anda akan membuat salinan sesuatu dalam suatu fungsi, biarkan kompiler melakukannya di daftar parameter. ‡)
Bagaimanapun, metode untuk memperoleh sumber daya ini adalah kunci untuk menghilangkan duplikasi kode: kita bisa menggunakan kode dari copy-constructor untuk membuat salinan, dan tidak perlu mengulang sedikit pun. Sekarang setelah salinan dibuat, kami siap bertukar.
Perhatikan bahwa saat memasuki fungsi, semua data baru sudah dialokasikan, 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 secara manual sebelumnya untuk jaminan pengecualian yang kuat, kompiler lakukan untuk kami sekarang; baik sekali.)Pada titik ini kami bebas-rumah, karena
swap
tidak melempar. Kami menukar data kami saat ini dengan data yang disalin, dengan aman mengubah keadaan kami, dan data lama dimasukkan ke dalam sementara. Data lama kemudian dirilis ketika fungsi kembali. (Dimana lingkup parameter berakhir dan destruktornya disebut.)Karena idiom tidak mengulangi kode, kami tidak dapat memperkenalkan bug di dalam operator. Perhatikan bahwa ini berarti kita menyingkirkan kebutuhan untuk pemeriksaan penugasan sendiri, memungkinkan implementasi seragam tunggal
operator=
. (Selain itu, kami tidak lagi memiliki penalti kinerja untuk penugasan non-mandiri.)Dan itu adalah idiom copy-and-swap.
Bagaimana dengan C ++ 11?
Versi selanjutnya dari C ++, C ++ 11, membuat satu perubahan yang sangat penting untuk bagaimana kita mengelola sumber daya: Aturan Tiga sekarang adalah Aturan Empat (dan setengah). Mengapa? Karena kita tidak hanya perlu menyalin-membangun sumber daya kita , kita juga perlu memindahkan-membangunnya .
Beruntung bagi kami, ini mudah:
Apa yang terjadi di sini? Ingat tujuan konstruksi-bergerak: untuk mengambil sumber daya dari instance kelas yang lain, membiarkannya dalam keadaan dijamin dapat ditugaskan dan dapat dirusak.
Jadi apa yang kami lakukan adalah sederhana: menginisialisasi melalui konstruktor default (fitur C ++ 11), lalu bertukar dengan
other
; kita tahu instance bawaan kelas kita dapat dengan aman ditugaskan dan dihancurkan, jadi kita tahuother
akan dapat melakukan hal yang sama, setelah bertukar.(Perhatikan bahwa beberapa kompiler tidak mendukung delegasi konstruktor; dalam hal ini, kita harus secara manual membangun kelas. Ini adalah tugas yang tidak menguntungkan tetapi untungnya sepele.)
Mengapa itu berhasil?
Itulah satu-satunya perubahan yang perlu kita lakukan untuk kelas kita, jadi mengapa itu berhasil? Ingat keputusan penting yang kami buat untuk menjadikan parameter nilai dan bukan referensi:
Sekarang, jika
other
diinisialisasi dengan suatu nilai, itu akan dipindahkan-dibangun . Sempurna. Dengan cara yang sama C ++ 03 mari kita gunakan kembali fungsionalitas copy-constructor kita dengan mengambil argumen berdasarkan-nilai, C ++ 11 akan secara otomatis memilih move-constructor jika perlu juga. (Dan, tentu saja, seperti yang disebutkan dalam artikel yang ditautkan sebelumnya, penyalinan / pemindahan nilai dapat saja dihilangkan sama sekali.)Dan dengan demikian menyimpulkan idiom copy-and-swap.
Catatan kaki
* Mengapa kita menetapkan
mArray
ke nol? Karena jika ada kode lebih lanjut dalam operator melempar, penghancurdumb_array
mungkin disebut; dan jika itu terjadi tanpa menyetelnya ke nol, kami berusaha menghapus memori yang sudah dihapus! Kami menghindari ini dengan menetapkannya ke nol, karena menghapus nol adalah tanpa operasi.† Ada klaim lain bahwa kita harus mengkhususkan diri
std::swap
untuk tipe kita, menyediakan fungsi bersama di dalam kelasswap
di sampingswap
, dll. Tapi ini semua tidak perlu: setiap penggunaan yang tepatswap
akan melalui panggilan yang tidak memenuhi syarat, dan fungsi kita akan menjadi ditemukan melalui ADL . Satu fungsi akan dilakukan.‡ Alasannya sederhana: sekali Anda memiliki sumber daya untuk diri sendiri, Anda dapat bertukar dan / atau memindahkannya (C ++ 11) di mana pun ia perlu. Dan dengan membuat salinan dalam daftar parameter, Anda memaksimalkan pengoptimalan.
†† Konstruktor pemindahan seharusnya secara umum
noexcept
, jika tidak beberapa kode (misalnyastd::vector
mengubah ukuran logika) akan menggunakan copy constructor bahkan ketika sebuah langkah masuk akal. Tentu saja, tandai saja kecuali jika kode di dalamnya tidak melempar pengecualian.sumber
swap
ditemukan selama ADL jika Anda ingin itu berfungsi di sebagian besar kode generik yang akan Anda temui, sepertiboost::swap
dan berbagai instance swap lainnya. Swap adalah masalah rumit di C ++, dan umumnya kita semua sepakat bahwa satu titik akses adalah yang terbaik (untuk konsistensi), dan satu-satunya cara untuk melakukan itu secara umum adalah fungsi bebas (int
tidak dapat memiliki anggota swap, sebagai contoh). Lihat pertanyaan saya untuk beberapa latar belakang.Tugas, pada intinya, adalah dua langkah: menghancurkan keadaan lama objek dan membangun keadaan baru sebagai salinan dari keadaan beberapa objek lain.
Pada dasarnya, itulah yang dilakukan destructor dan copy constructor , jadi ide pertama adalah mendelegasikan pekerjaan kepada mereka. Namun, karena kerusakan tidak boleh gagal, sementara konstruksi mungkin, kami sebenarnya ingin melakukannya sebaliknya : pertama melakukan bagian yang konstruktif dan, jika itu berhasil, maka lakukan bagian yang merusak . Idi copy-and-swap adalah cara untuk melakukan hal itu: Pertama memanggil konstruktor copy kelas untuk membuat objek sementara, kemudian menukar datanya dengan yang sementara, dan kemudian membiarkan destruktor sementara menghancurkan negara lama.
Sejak
swap()
seharusnya tidak pernah gagal, satu-satunya bagian yang mungkin gagal adalah pembuatan salinan. Itu dilakukan pertama kali, dan jika gagal, tidak ada yang akan berubah di objek yang ditargetkan.Dalam bentuknya yang disempurnakan, copy-and-swap dilaksanakan dengan membuat salinan dilakukan dengan menginisialisasi parameter (non-referensi) dari operator penugasan:
sumber
std::swap(this_string, that)
tidak memberikan jaminan tanpa-lemparan. Ini memberikan keamanan pengecualian yang kuat, tetapi bukan jaminan tanpa-lemparan.std::string::swap
(yang disebut denganstd::swap
). Dalam C ++ 0x,std::string::swap
adalahnoexcept
dan tidak boleh melempar pengecualian.std::array
...)Sudah ada beberapa jawaban bagus. Saya akan fokus terutama pada apa yang saya pikir mereka kurang - penjelasan tentang "kontra" dengan idiom copy-and-swap ....
Cara menerapkan operator penugasan dalam hal fungsi swap:
Ide dasarnya adalah:
bagian yang paling rawan kesalahan dalam menetapkan suatu objek adalah memastikan sumber daya apa pun yang dibutuhkan oleh kondisi baru (mis. memori, deskriptor)
akuisisi yang dapat dicoba sebelum memodifikasi keadaan objek saat ini (yaitu
*this
) jika salinan nilai baru dibuat, yang mengaparhs
diterima oleh nilai (yaitu disalin) daripada dengan referensimenukar keadaan salinan lokal
rhs
dan*this
adalah biasanya relatif mudah dilakukan tanpa potensi kegagalan / pengecualian, mengingat salinan lokal tidak membutuhkan negara tertentu setelah itu (hanya perlu negara cocok untuk destructor untuk lari, sebanyak untuk objek yang bergerak dari dalam> = C ++ 11)Ketika Anda ingin orang yang dituju keberatan tidak terpengaruh oleh penugasan yang melempar pengecualian, dengan asumsi Anda memiliki atau dapat menulis
swap
dengan jaminan pengecualian yang kuat, dan idealnya yang tidak dapat gagal /throw
.. †Saat Anda menginginkan cara yang bersih, mudah dimengerti, dan kuat untuk mendefinisikan operator penugasan dalam hal konstruktor penyalinan (sederhana),
swap
dan fungsi destruktor.†
swap
melempar: umumnya memungkinkan untuk bertukar anggota data yang dapat dipercaya objek dilacak oleh pointer, tetapi anggota data non-pointer yang tidak memiliki swap bebas-lempar, atau yang swapping harus diimplementasikan sebagaiX tmp = lhs; lhs = rhs; rhs = tmp;
dan menyalin konstruksi atau penugasan mungkin melempar, masih berpotensi gagal meninggalkan beberapa anggota data bertukar dan yang lainnya tidak. Potensi ini berlaku bahkan untuk C ++ 03std::string
ketika James mengomentari jawaban lain:Implementation implementasi operator penugasan yang tampaknya waras ketika menugaskan dari objek yang berbeda dapat dengan mudah gagal untuk penugasan sendiri. Walaupun mungkin tampak tidak terbayangkan bahwa kode klien bahkan akan mencoba penugasan sendiri, itu dapat terjadi relatif mudah selama operasi algo pada wadah, dengan
x = f(x);
kode di manaf
(mungkin hanya untuk beberapa#ifdef
cabang) makro#define f(x) x
atau fungsi yang mengembalikan referensi kex
, atau bahkan (sepertinya tidak efisien tapi ringkas) kode sukax = c1 ? x * 2 : c2 ? x / 2 : x;
). Sebagai contoh:Pada penugasan sendiri, penghapusan kode di atas
x.p_;
, menunjukp_
pada wilayah tumpukan yang baru dialokasikan, kemudian mencoba membaca data yang belum diinisialisasi di dalamnya (Perilaku Tidak Terdefinisi), jika itu tidak melakukan sesuatu yang terlalu aneh,copy
mencoba penugasan sendiri untuk setiap just- menghancurkan 'T'!Idi Idi copy-and-swap dapat menimbulkan inefisiensi atau keterbatasan karena penggunaan sementara sementara (ketika parameter operator dibuat-salin):
Di sini, tulisan tangan
Client::operator=
mungkin memeriksa apakah*this
sudah terhubung ke server yang sama denganrhs
(mungkin mengirim kode "reset" jika berguna), sedangkan pendekatan copy-and-swap akan memanggil copy-constructor yang kemungkinan akan ditulis untuk dibuka koneksi soket yang berbeda kemudian tutup yang asli. Tidak hanya itu bisa berarti interaksi jaringan jarak jauh dan bukan salinan variabel dalam proses yang sederhana, tetapi juga bisa bertabrakan dengan batasan klien atau server pada sumber daya soket atau koneksi. (Tentu saja kelas ini memiliki antarmuka yang cukup mengerikan, tapi itu masalah lain ;-P).sumber
Client
adalah bahwa penugasan tidak dilarang.Jawaban ini lebih seperti tambahan dan sedikit modifikasi untuk jawaban di atas.
Dalam beberapa versi Visual Studio (dan mungkin kompiler lain) ada bug yang sangat mengganggu dan tidak masuk akal. Jadi jika Anda mendeklarasikan / mendefinisikan
swap
fungsi Anda seperti ini:... kompiler akan meneriaki Anda saat Anda memanggil
swap
fungsi:Ini ada hubungannya dengan
friend
fungsi yang dipanggil danthis
objek dilewatkan sebagai parameter.Cara untuk mengatasi ini adalah dengan tidak menggunakan
friend
kata kunci dan mendefinisikan kembaliswap
fungsi:Kali ini, Anda bisa menelepon
swap
dan mengirim masukother
, sehingga membuat kompiler senang:Lagi pula, Anda tidak perlu menggunakan
friend
fungsi untuk menukar 2 objek. Masuk akal juga untuk membuatswap
fungsi anggota yang memiliki satuother
objek sebagai parameter.Anda sudah memiliki akses ke
this
objek, sehingga meneruskannya sebagai parameter secara teknis berlebihan.sumber
friend
fungsi dipanggil dengan*this
parameterSaya ingin menambahkan kata peringatan ketika Anda berhadapan dengan wadah ++ yang sadar-gaya C ++. Swapping dan tugas memiliki semantik yang berbeda.
Untuk konkret, mari kita pertimbangkan sebuah wadah
std::vector<T, A>
, di manaA
ada beberapa jenis pengalokasi stateful, dan kami akan membandingkan fungsi-fungsi berikut:Tujuan dari kedua fungsi
fs
danfm
adalah untuk memberikana
negara yangb
awalnya. Namun, ada pertanyaan tersembunyi: Apa yang terjadi jikaa.get_allocator() != b.get_allocator()
? Jawabannya adalah, tergantung. Ayo kita tulisAT = std::allocator_traits<A>
.Jika
AT::propagate_on_container_move_assignment
inistd::true_type
, makafm
reassigns pengalokasi daria
dengan nilaib.get_allocator()
, jika tidak maka tidak, dana
terus menggunakan pengalokasi aslinya. Dalam hal ini, elemen data perlu ditukar secara individual, karena penyimpanana
danb
tidak kompatibel.Jika
AT::propagate_on_container_swap
inistd::true_type
, makafs
swap data dan penyalur dengan cara yang diharapkan.Jika
AT::propagate_on_container_swap
inistd::false_type
, maka kita perlu cek dinamis.a.get_allocator() == b.get_allocator()
, maka kedua wadah menggunakan penyimpanan yang kompatibel, dan bertukar hasil dengan cara biasa.a.get_allocator() != b.get_allocator()
, program tersebut memiliki perilaku yang tidak terdefinisi (lih. [Container.requirements.general / 8].Hasilnya adalah bahwa swapping telah menjadi operasi non-sepele di C ++ 11 segera setelah wadah Anda mulai mendukung pengalokasi keadaan. Itu agak "kasus penggunaan lanjutan", tapi itu tidak sepenuhnya tidak mungkin, karena optimasi gerakan biasanya hanya menjadi menarik setelah kelas Anda mengelola sumber daya, dan memori adalah salah satu sumber daya paling populer.
sumber