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 yang std::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:
#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 mengatur array, tetapi perlu operator=
bekerja dengan benar.
Solusi yang gagal
Begini tampilan implementasi yang naif:
// 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;
}
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:
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;
}
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 menyediakan swap
fungsi .
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);
}
// ...
};
( Inilah penjelasan mengapa public friend swap
.) Sekarang kita tidak hanya dapat menukar milik kita dumb_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:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
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):
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
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:
class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
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 tahu other
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:
dumb_array& operator=(dumb_array other); // (1)
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, penghancur dumb_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 kelas swap
di samping swap
, dll. Tapi ini semua tidak perlu: setiap penggunaan yang tepat swap
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 (misalnya std::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.