Salin konstruktor dan = operator overload di C ++: apakah fungsi umum mungkin?

87

Sejak konstruktor salinan

MyClass(const MyClass&);

dan operator = kelebihan beban

MyClass& operator = (const MyClass&);

memiliki kode yang hampir sama, parameter yang sama, dan hanya berbeda pada bagian pengembalian, apakah mungkin memiliki fungsi yang sama untuk digunakan keduanya?

MPelletier
sumber
6
"... memiliki kode yang hampir sama ..."? Hmm ... Anda pasti melakukan sesuatu yang salah. Cobalah untuk meminimalkan kebutuhan untuk menggunakan fungsi yang ditentukan pengguna untuk ini dan biarkan kompilator melakukan semua pekerjaan kotor. Ini sering kali berarti merangkum sumber daya dalam objek anggota mereka sendiri. Anda bisa menunjukkan kepada kami beberapa kode. Mungkin kami punya beberapa saran desain yang bagus.
jual

Jawaban:

123

Iya. Ada dua opsi umum. Satu - yang umumnya tidak disarankan - adalah memanggil operator=dari konstruktor salinan secara eksplisit:

MyClass(const MyClass& other)
{
    operator=(other);
}

Namun, menyediakan barang operator=merupakan tantangan ketika harus menghadapi keadaan lama dan masalah yang timbul dari penugasan diri. Selain itu, semua anggota dan basis mendapatkan default diinisialisasi terlebih dahulu meskipun mereka akan ditetapkan dari other. Ini bahkan mungkin tidak berlaku untuk semua anggota dan basis dan bahkan jika valid itu secara semantik berlebihan dan mungkin secara praktis mahal.

Solusi yang semakin populer adalah mengimplementasikan operator=menggunakan konstruktor salinan dan metode swap.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

atau bahkan:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

SEBUAH swap fungsi biasanya mudah ditulis karena hanya menukar kepemilikan internal dan tidak harus membersihkan status yang ada atau mengalokasikan sumber daya baru.

Keuntungan dari idiom salin dan swap adalah bahwa ia secara otomatis aman untuk penempatan sendiri dan - asalkan operasi swap tidak ada lemparan - juga sangat aman untuk pengecualian.

Untuk menjadi pengecualian yang aman, operator tugas tertulis 'tangan' biasanya harus mengalokasikan salinan sumber daya baru sebelum mengalokasikan sumber daya lama penerima hak sehingga jika terjadi pengecualian mengalokasikan sumber daya baru, status lama masih dapat dikembalikan ke . Semua ini gratis dengan copy-and-swap tetapi biasanya lebih kompleks, dan karenanya rawan kesalahan, untuk dilakukan dari awal.

Satu hal yang harus diperhatikan adalah memastikan bahwa metode swap adalah swap yang benar, dan bukan default std::swap yang menggunakan konstruktor salinan dan operator penugasan itu sendiri.

Biasanya swapdigunakan memberwise . std::swapberfungsi dan dijamin 'tanpa lemparan' dengan semua tipe dasar dan tipe penunjuk. Sebagian besar petunjuk cerdas juga dapat ditukar dengan jaminan tanpa lemparan.

CB Bailey
sumber
3
Sebenarnya, mereka bukanlah operasi biasa. Saat copy ctor pertama kali menginisialisasi anggota objek, operator penugasan menimpa nilai yang ada. Mempertimbangkan hal ini, semua operator=dari copy ctor sebenarnya sangat buruk, karena pertama-tama menginisialisasi semua nilai ke beberapa default hanya untuk menimpanya dengan nilai objek lain tepat setelahnya.
sbi
14
Mungkin untuk "Saya tidak merekomendasikan", tambahkan "dan begitu pula ahli C ++". Seseorang mungkin datang dan gagal untuk menyadari bahwa Anda tidak hanya mengungkapkan preferensi minoritas pribadi, tetapi juga pendapat konsensus yang telah ditetapkan dari mereka yang benar-benar memikirkannya. Dan, oke, mungkin saya salah dan beberapa ahli C ++ memang merekomendasikannya, tetapi secara pribadi saya masih memberikan tantangan bagi seseorang untuk memberikan referensi untuk rekomendasi itu.
Steve Jessop
4
Cukup adil, toh saya sudah memberi suara positif kepada Anda :-). Saya pikir jika sesuatu secara luas dianggap sebagai praktik terbaik, maka yang terbaik adalah mengatakannya (dan lihat lagi jika seseorang mengatakan itu bukan yang terbaik). Demikian juga jika seseorang bertanya "apakah mungkin menggunakan mutex dalam C ++", saya tidak akan mengatakan "satu opsi yang cukup umum adalah mengabaikan RAII sepenuhnya, dan menulis kode non-pengecualian-aman yang menemui jalan buntu dalam produksi, tetapi semakin populer untuk menulis kode yang layak dan berfungsi ";-)
Steve Jessop
4
+1. Dan saya pikir selalu ada analisis yang dibutuhkan. Saya pikir masuk akal untuk memiliki assignfungsi anggota yang digunakan oleh copy ctor dan operator penugasan dalam beberapa kasus (untuk kelas ringan). Dalam kasus lain (kasus intensif / penggunaan sumber daya, handle / body) salinan / swap tentu saja adalah cara yang harus dilakukan.
Johannes Schaub - litb
2
@ litb: Saya terkejut dengan ini jadi saya mencari Item 41 di Exception C ++ (yang menjadi dasar ini) dan rekomendasi khusus ini telah hilang dan dia merekomendasikan copy-and-swap sebagai gantinya. Dengan agak sembunyi-sembunyi dia menjatuhkan "Masalah # 4: Ini Tidak Efisien untuk Penugasan" pada saat yang sama.
CB Bailey
14

Konstruktor salinan melakukan inisialisasi objek yang digunakan untuk menjadi memori mentah untuk pertama kalinya. Operator penugasan, OTOH, menimpa nilai yang ada dengan yang baru. Lebih sering daripada tidak sama sekali, ini melibatkan penghentian sumber daya lama (misalnya, memori) dan mengalokasikan yang baru.

Jika ada kesamaan di antara keduanya, operator penugasan melakukan penghancuran dan konstruksi salinan. Beberapa pengembang dulu benar-benar menerapkan tugas dengan penghancuran di tempat diikuti dengan konstruksi salinan penempatan. Namun, ini ide yang sangat buruk. (Bagaimana jika ini adalah operator penugasan dari kelas dasar yang dipanggil selama penugasan kelas turunan?)

Apa yang biasanya dianggap idiom kanonik saat ini digunakan swapseperti yang disarankan Charles:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Ini menggunakan konstruksi-salinan (catatan yang otherdisalin) dan kehancuran (itu dihancurkan di akhir fungsi) - dan menggunakannya dalam urutan yang benar, juga: konstruksi (mungkin gagal) sebelum kehancuran (tidak boleh gagal).

sbi
sumber
Haruskah swapdiumumkan virtual?
1
@Johannes: Fungsi virtual digunakan dalam hierarki kelas polimorfik. Operator penugasan digunakan untuk tipe nilai. Keduanya hampir tidak bercampur.
sbi
-3

Sesuatu yang menggangguku tentang:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Pertama, membaca kata "swap" ketika pikiran saya memikirkan "salinan" mengganggu akal sehat saya. Juga, saya mempertanyakan tujuan dari trik mewah ini. Ya, pengecualian apa pun dalam membangun sumber daya baru (disalin) harus terjadi sebelum pertukaran, yang tampaknya merupakan cara yang aman untuk memastikan semua data baru diisi sebelum membuatnya ditayangkan.

Tidak apa-apa. Jadi, bagaimana dengan pengecualian yang terjadi setelah penukaran? (ketika sumber daya lama dihancurkan ketika objek sementara keluar dari ruang lingkup) Dari perspektif pengguna tugas, operasi gagal, kecuali tidak. Ini memiliki efek samping yang besar: salinan itu benar-benar terjadi. Hanya beberapa pembersihan sumber daya yang gagal. Keadaan objek tujuan telah diubah meskipun operasi dari luar tampaknya gagal.

Jadi, saya mengusulkan daripada "swap" untuk melakukan "transfer" yang lebih alami:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Masih ada konstruksi objek sementara, tetapi tindakan segera berikutnya adalah membebaskan semua sumber daya tujuan saat ini sebelum memindahkan (dan NULLing sehingga mereka tidak akan membebaskan dua kali) sumber daya dari sumber itu.

Alih-alih {membangun, memindahkan, merusak}, saya mengusulkan {membangun, merusak, memindahkan}. Tindakan, yang merupakan tindakan paling berbahaya, adalah tindakan yang dilakukan terakhir setelah segala sesuatu diselesaikan.

Ya, kerusakan gagal adalah masalah di kedua skema. Data bisa rusak (disalin ketika Anda tidak mengira itu) atau hilang (dibebaskan ketika Anda tidak mengira itu). Hilang lebih baik dari pada rusak. Tidak ada data yang lebih baik dari data buruk.

Transfer, bukan swap. Itu saran saya.

Matthew
sumber
2
Penghancur tidak boleh gagal, jadi pengecualian atas penghancuran tidak diharapkan. Dan, saya tidak mengerti apa keuntungan dari memindahkan gerakan di belakang kehancuran, jika gerakan adalah operasi yang paling berbahaya? Yaitu, dalam skema standar, kegagalan pemindahan tidak akan merusak status lama, sedangkan skema baru Anda tidak. Jadi kenapa? Juga, First, reading the word "swap" when my mind is thinking "copy" irritates-> Sebagai penulis perpustakaan, Anda biasanya tahu praktik umum (salin + tukar), dan intinya adalah my mind. Pikiran Anda sebenarnya tersembunyi di balik antarmuka publik. Itulah semua tentang kode yang dapat digunakan kembali.
Sebastian Mach