Apa idiom copy-and-swap?

2003

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:

GManNickG
sumber
7
gotw.ca/gotw/059.htm dari Herb Sutter
DumbCoder
2
Luar biasa, saya menautkan pertanyaan ini dari jawaban saya untuk memindahkan semantik .
fredoverflow
4
Ide bagus untuk memiliki penjelasan lengkap untuk idiom ini, sangat umum bahwa setiap orang harus mengetahuinya.
Matthieu M.
16
Peringatan: Idi copy / swap digunakan jauh lebih sering daripada berguna. Ini sering merusak kinerja ketika jaminan keselamatan pengecualian yang kuat tidak diperlukan dari penugasan salinan. Dan ketika keamanan pengecualian yang kuat diperlukan untuk penugasan penyalinan, ia dengan mudah disediakan oleh fungsi generik pendek, di samping operator penugasan penyalinan yang jauh lebih cepat. Lihat slideshare.net/ripplelabs/howard-hinnant-accu2014 slide 43 - 53. Ringkasan: salin / tukar adalah alat yang berguna di kotak alat. Tetapi telah dipasarkan secara berlebihan dan selanjutnya sering disalahgunakan.
Howard Hinnant
2
@ HowardHinnant: Ya, +1 untuk itu. Saya menulis ini pada waktu di mana hampir setiap pertanyaan C ++ adalah "membantu kelas saya crash ketika menyalinnya" dan ini adalah tanggapan saya. Ini tepat ketika Anda hanya ingin bekerja menyalin- / pindah-semantik atau apa pun sehingga Anda dapat beralih ke hal-hal lain, tetapi itu tidak benar-benar optimal. Jangan ragu untuk meletakkan penafian di bagian atas jawaban saya jika Anda pikir itu akan membantu.
GManNickG

Jawaban:

2184

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 swapfungsi, 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 swapfungsi.

Fungsi swap adalah fungsi non-melempar yang menukar dua objek kelas, anggota untuk anggota. Kita mungkin tergoda untuk menggunakan std::swapalih-alih menyediakan milik kita sendiri, tetapi ini tidak mungkin; std::swapmenggunakan 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 swapakan menggunakan operator swap kustom kami, melompati konstruksi yang tidak perlu dan penghancuran kelas kami yang std::swapakan 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).

  1. 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.

  2. Yang kedua adalah bahwa itu hanya memberikan jaminan pengecualian dasar. Jika new int[mSize]gagal, *thispasti 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;
    }
  3. 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/ catchklausa, 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: swapfungsi. 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 swapfungsi .

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 swaptidak 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 otherakan 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 otherdiinisialisasi 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 mArrayke nol? Karena jika ada kode lebih lanjut dalam operator melempar, penghancur dumb_arraymungkin 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::swapuntuk tipe kita, menyediakan fungsi bersama di dalam kelas swapdi samping swap, dll. Tapi ini semua tidak perlu: setiap penggunaan yang tepat swapakan 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::vectormengubah ukuran logika) akan menggunakan copy constructor bahkan ketika sebuah langkah masuk akal. Tentu saja, tandai saja kecuali jika kode di dalamnya tidak melempar pengecualian.

GManNickG
sumber
17
@ GM: Saya berpendapat bahwa kelas yang mengelola beberapa sumber daya sekaligus akan gagal (kecuali keselamatan menjadi mimpi buruk) dan saya akan sangat menyarankan agar kelas mengelola SATU sumber daya ATAU memiliki fungsi bisnis dan menggunakan manajer.
Matthieu M.
22
Saya tidak mengerti mengapa metode swap dinyatakan sebagai teman di sini?
szx
9
@ asd: Untuk memungkinkannya ditemukan melalui ADL.
GManNickG
8
@neuviemeporte: Dengan tanda kurung, elemen array default diinisialisasi. Tanpa, mereka tidak diinisialisasi. Karena dalam copy constructor kita akan menimpa nilai-nilai itu, kita dapat melewatkan inisialisasi.
GManNickG
10
@neuviemeporte: Anda perlu swapditemukan selama ADL jika Anda ingin itu berfungsi di sebagian besar kode generik yang akan Anda temui, seperti boost::swapdan 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 ( inttidak dapat memiliki anggota swap, sebagai contoh). Lihat pertanyaan saya untuk beberapa latar belakang.
GManNickG
274

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.
Sejakswap()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:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
sbi
sumber
1
Saya pikir menyebutkan menyebutkan jerawat sama pentingnya dengan menyebutkan salinan, pertukaran dan penghancuran. Swap tidak secara ajaib pengecualian aman. Itu pengecualian-aman karena penukar pointer adalah pengecualian-aman. Anda tidak harus menggunakan jerawat, tetapi jika Anda tidak melakukannya, Anda harus memastikan bahwa setiap pertukaran anggota adalah pengecualian aman. Itu bisa menjadi mimpi buruk ketika anggota ini dapat berubah dan itu sepele ketika mereka tersembunyi di balik jerawat. Dan kemudian, kemudian muncul biaya jerawat. Yang membawa kita pada kesimpulan bahwa sering pengecualian-keselamatan menanggung biaya dalam kinerja.
wilhelmtell
7
std::swap(this_string, that)tidak memberikan jaminan tanpa-lemparan. Ini memberikan keamanan pengecualian yang kuat, tetapi bukan jaminan tanpa-lemparan.
wilhelmtell
11
@wilhelmtell: Dalam C ++ 03, tidak disebutkan pengecualian yang berpotensi dilontarkan oleh std::string::swap(yang disebut dengan std::swap). Dalam C ++ 0x, std::string::swapadalah noexceptdan tidak boleh melempar pengecualian.
James McNellis
2
@sbi @JamesMcNellis ok, tapi intinya masih berlaku: jika Anda memiliki anggota tipe kelas, Anda harus memastikan bertukar mereka adalah no-throw. Jika Anda memiliki satu anggota yang merupakan penunjuk maka itu sepele. Kalau tidak, tidak.
wilhelmtell
2
@wilhelmtell: Saya pikir itu adalah titik bertukar: tidak pernah melempar dan selalu O (1) (yeah, saya tahu, std::array...)
sbi
44

Sudah ada beberapa jawaban bagus. Saya akan fokus terutama pada apa yang saya pikir mereka kurang - penjelasan tentang "kontra" dengan idiom copy-and-swap ....

Apa idiom copy-and-swap?

Cara menerapkan operator penugasan dalam hal fungsi swap:

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

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 mengapa rhsditerima oleh nilai (yaitu disalin) daripada dengan referensi

  • menukar keadaan salinan lokal rhsdan *thisadalah 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)

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

  • Ketika Anda ingin orang yang dituju keberatan tidak terpengaruh oleh penugasan yang melempar pengecualian, dengan asumsi Anda memiliki atau dapat menulis swapdengan 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), swapdan fungsi destruktor.

    • Penugasan sendiri dilakukan sebagai copy-and-swap untuk menghindari kasus tepi yang sering diabaikan. ‡

  • Ketika ada penalti kinerja atau penggunaan sumber daya sesaat lebih tinggi diciptakan dengan memiliki objek sementara tambahan selama penugasan tidak penting untuk aplikasi Anda. ⁂

swapmelempar: 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 sebagai X 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 ++ 03 std::stringketika James mengomentari jawaban lain:

@wilhelmtell: Di C ++ 03, tidak disebutkan pengecualian yang berpotensi dibuang oleh std :: string :: swap (yang disebut dengan std :: swap). Dalam C ++ 0x, std :: string :: swap adalah noexcept dan tidak boleh melempar pengecualian. - James McNellis 22 Des 10 'jam 15:24


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 mana f(mungkin hanya untuk beberapa #ifdefcabang) makro #define f(x) xatau fungsi yang mengembalikan referensi ke x, atau bahkan (sepertinya tidak efisien tapi ringkas) kode suka 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_);
    }
    ...
};

Pada penugasan sendiri, penghapusan kode di atas x.p_;, menunjuk p_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, copymencoba 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):

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

Di sini, tulisan tangan Client::operator=mungkin memeriksa apakah *thissudah terhubung ke server yang sama dengan rhs(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).

Tony Delroy
sumber
4
Yang mengatakan, koneksi soket hanyalah sebuah contoh - prinsip yang sama berlaku untuk setiap inisialisasi yang berpotensi mahal, seperti penyelidikan / inisialisasi / kalibrasi perangkat keras, menghasilkan kumpulan benang atau angka acak, tugas kriptografi tertentu, cache, pemindaian sistem file, basis data koneksi dll.
Tony Delroy
Ada satu lagi (besar) con. Pada spesifikasi saat ini secara teknis objek tidak akan memiliki operator penugasan bergerak! Jika nanti digunakan sebagai anggota suatu kelas, kelas baru tidak akan memiliki gerakan-ctor yang dihasilkan secara otomatis! Sumber: youtu.be/mYrbivnruYw?t=43m14s
user362515
3
Masalah utama dengan operator penugasan salinan Clientadalah bahwa penugasan tidak dilarang.
sbi
Dalam contoh klien, kelas harus dibuat noncopyable.
John Z. Li
25

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 swapfungsi Anda seperti ini:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... kompiler akan meneriaki Anda saat Anda memanggil swapfungsi:

masukkan deskripsi gambar di sini

Ini ada hubungannya dengan friendfungsi yang dipanggil dan thisobjek dilewatkan sebagai parameter.


Cara untuk mengatasi ini adalah dengan tidak menggunakan friendkata kunci dan mendefinisikan kembali swapfungsi:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Kali ini, Anda bisa menelepon swap dan mengirim masuk other, sehingga membuat kompiler senang:

masukkan deskripsi gambar di sini


Lagi pula, Anda tidak perlu menggunakan friendfungsi untuk menukar 2 objek. Masuk akal juga untuk membuat swapfungsi anggota yang memiliki satu otherobjek sebagai parameter.

Anda sudah memiliki akses ke this objek, sehingga meneruskannya sebagai parameter secara teknis berlebihan.

Oleksiy
sumber
1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitle.jpg . Ini adalah versi yang disederhanakan. Kesalahan tampaknya terjadi setiap kali friendfungsi dipanggil dengan *thisparameter
Oleksiy
1
@ GMNickG seperti yang saya katakan, ini adalah bug dan mungkin bekerja dengan baik untuk orang lain. Saya hanya ingin membantu beberapa orang yang mungkin memiliki masalah yang sama dengan saya. Saya mencoba ini dengan Visual Studio 2012 Express dan 2013 Preview dan satu-satunya hal yang membuatnya pergi, adalah modifikasi saya
Oleksiy
8
@GManNickG tidak akan cocok dengan komentar dengan semua gambar dan contoh kode. Dan tidak masalah jika orang downvote, saya yakin ada seseorang di luar sana yang mendapatkan bug yang sama; informasi dalam posting ini mungkin hanya apa yang mereka butuhkan.
Oleksiy
14
perhatikan bahwa ini hanya bug dalam penyorotan kode IDE (IntelliSense) ... Ini akan dikompilasi dengan baik tanpa peringatan / kesalahan.
Amro
3
Silakan laporkan bug VS di sini jika Anda belum melakukannya (dan jika belum diperbaiki) connect.microsoft.com/VisualStudio
Matt
15

Saya 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 mana Aada beberapa jenis pengalokasi stateful, dan kami akan membandingkan fungsi-fungsi berikut:

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

Tujuan dari kedua fungsi fsdan fmadalah untuk memberikan anegara yang bawalnya. Namun, ada pertanyaan tersembunyi: Apa yang terjadi jika a.get_allocator() != b.get_allocator()? Jawabannya adalah, tergantung. Ayo kita tulis AT = std::allocator_traits<A>.

  • Jika AT::propagate_on_container_move_assignment ini std::true_type, maka fmreassigns pengalokasi dari adengan nilai b.get_allocator(), jika tidak maka tidak, dan aterus menggunakan pengalokasi aslinya. Dalam hal ini, elemen data perlu ditukar secara individual, karena penyimpanan adan btidak kompatibel.

  • Jika AT::propagate_on_container_swapini std::true_type, maka fsswap data dan penyalur dengan cara yang diharapkan.

  • Jika AT::propagate_on_container_swapini std::false_type, maka kita perlu cek dinamis.

    • Jika a.get_allocator() == b.get_allocator(), maka kedua wadah menggunakan penyimpanan yang kompatibel, dan bertukar hasil dengan cara biasa.
    • Namun, jika 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.

Kerrek SB
sumber