Kapan kita harus menggunakan konstruktor salinan?

87

Saya tahu bahwa compiler C ++ membuat salinan konstruktor untuk kelas. Dalam hal apa kita harus menulis konstruktor salinan yang ditentukan pengguna? Bisakah Anda memberikan beberapa contoh?

penguru
sumber
1
Salah satu kasus untuk menulis copy-ctor Anda sendiri: Ketika Anda harus melakukan deep copy. Perhatikan juga bahwa segera setelah Anda membuat ctor, tidak ada ctor default yang dibuat untuk Anda (kecuali Anda menggunakan kata kunci default).
harshvchawla

Jawaban:

75

Konstruktor salinan yang dihasilkan oleh kompilator melakukan penyalinan berdasarkan anggota. Terkadang itu tidak cukup. Sebagai contoh:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

dalam hal ini penyalinan anggota yang bijaksana storedtidak akan menduplikasi buffer (hanya penunjuk yang akan disalin), jadi yang pertama akan dihancurkan, salinan yang berbagi buffer akan delete[]berhasil dipanggil dan yang kedua akan mengalami perilaku tidak terdefinisi. Anda perlu menyalin konstruktor salinan dalam (dan operator tugas juga).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}
gigi tajam
sumber
10
Itu tidak bekerja sedikit-bijaksana, tapi salinan bijaksana anggota yang secara khusus memanggil copy-ctor untuk anggota tipe kelas.
Georg Fritzsche
7
Jangan tulis operator assingment seperti itu. Ini tidak terkecuali aman. (jika new melempar pengecualian, objek dibiarkan dalam keadaan tidak ditentukan dengan penyimpanan menunjuk ke bagian memori yang tidak dialokasikan (batalkan alokasi memori HANYA setelah semua operasi yang dapat melempar telah selesai dengan sukses)). Solusi sederhana adalah dengan menggunakan copy swap idium.
Martin York
@sharptooth baris ke-3 dari bawah yang Anda miliki delete stored[];dan saya percaya seharusnya begitudelete [] stored;
Peter Ajtai
4
Saya tahu ini hanya sebuah contoh, tetapi Anda harus menunjukkan solusi yang lebih baik adalah dengan menggunakan std::string. Ide umumnya adalah bahwa hanya kelas utilitas yang mengelola sumber daya yang perlu membebani Tiga Besar, dan semua kelas lain sebaiknya menggunakan kelas utilitas tersebut saja, sehingga tidak perlu mendefinisikan salah satu dari Tiga Besar.
GManNickG
2
@ Martin: Saya ingin memastikan itu diukir di batu. : P
GManNickG
46

Saya sedikit kesal karena aturan Rule of Fiveitu tidak dikutip.

Aturan ini sangat sederhana:

Aturan Lima :
Setiap kali Anda menulis salah satu Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor atau Move Assignment Operator, Anda mungkin perlu menulis empat lainnya.

Tetapi ada pedoman yang lebih umum yang harus Anda ikuti, yang berasal dari kebutuhan untuk menulis kode pengecualian-aman:

Setiap sumber daya harus dikelola oleh objek khusus

Berikut @sharptooth's kode masih (kebanyakan) baik-baik saja, namun jika ia menambahkan atribut kedua untuk kelasnya itu tidak akan. Pertimbangkan kelas berikut:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

Apa yang terjadi jika new Barmelempar? Bagaimana Anda menghapus objek yang ditunjuk mFoo? Ada solusi (coba tingkat fungsi / tangkap ...), mereka tidak skala.

Cara yang tepat untuk menghadapi situasi ini adalah dengan menggunakan kelas yang tepat daripada petunjuk mentah.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Dengan implementasi konstruktor yang sama (atau sebenarnya, menggunakan make_unique), sekarang saya memiliki keamanan pengecualian gratis !!! Bukankah itu menarik? Dan yang terbaik dari semuanya, saya tidak perlu lagi khawatir tentang destruktor yang tepat! Saya memang perlu menulis sendiri Copy Constructordan Assignment Operatormeskipun, karena unique_ptrtidak mendefinisikan operasi ini ... tetapi tidak masalah di sini;)

Dan karena itu, sharptoothkelas meninjau kembali:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Saya tidak tahu tentang Anda, tetapi saya menemukan milik saya lebih mudah;)

Matthieu M.
sumber
Untuk C ++ 11 - aturan lima yang menambahkan aturan tiga, Pindahkan Pembuat dan Operator Penugasan Pindah.
Robert Andrzejuk
1
@Robb: Perhatikan bahwa sebenarnya, seperti yang ditunjukkan pada contoh terakhir, Anda biasanya harus menargetkan Aturan Nol . Hanya kelas teknis khusus (generik) yang harus peduli tentang penanganan satu sumber daya, semua kelas lainnya harus menggunakan penunjuk / penampung cerdas tersebut dan tidak perlu mengkhawatirkannya.
Matthieu M.
@Bayu_joo Setuju :-) Saya menyebutkan Rule of Five, karena jawaban ini sebelum C ++ 11 dan dimulai dengan "Big Three", tetapi harus disebutkan bahwa sekarang "Big Five" relevan. Saya tidak ingin meremehkan jawaban ini karena benar dalam konteks yang ditanyakan.
Robert Andrzejuk
@ Robb: Poin bagus, saya memperbarui jawaban untuk menyebutkan Aturan Lima alih-alih Tiga Besar. Mudah-mudahan kebanyakan orang telah pindah ke kompiler C ++ 11 yang mampu sekarang (dan saya kasihan mereka yang masih belum).
Matthieu M.
31

Saya dapat mengingat dari praktik saya dan memikirkan kasus-kasus berikut ketika seseorang harus berurusan dengan secara eksplisit menyatakan / mendefinisikan konstruktor salinan. Saya telah mengelompokkan kasus menjadi dua kategori

  • Ketepatan / Semantik - jika Anda tidak menyediakan konstruktor salinan yang ditentukan pengguna, program yang menggunakan jenis tersebut mungkin gagal untuk dikompilasi, atau mungkin bekerja secara tidak benar.
  • Optimasi - menyediakan alternatif yang baik untuk konstruktor salinan yang dihasilkan kompiler memungkinkan untuk membuat program lebih cepat.


Ketepatan / Semantik

Saya menempatkan di bagian ini kasus-kasus di mana mendeklarasikan / mendefinisikan konstruktor salinan diperlukan untuk pengoperasian program yang benar menggunakan jenis itu.

Setelah membaca seluruh bagian ini, Anda akan mempelajari tentang beberapa kendala dalam mengizinkan kompilator untuk membuat konstruktor salinannya sendiri. Oleh karena itu, seperti yang dicatat oleh seand dalam jawabannya , selalu aman untuk menonaktifkan kemampuan copyability untuk kelas baru dan sengaja mengaktifkannya nanti saat benar-benar dibutuhkan.

Cara membuat kelas tidak dapat disalin di C ++ 03

Deklarasikan copy-konstruktor pribadi dan jangan sediakan implementasinya (sehingga build gagal pada tahap penautan meskipun objek jenis itu disalin dalam cakupan kelas sendiri atau oleh temannya).

Cara membuat kelas tidak dapat disalin di C ++ 11 atau yang lebih baru

Deklarasikan konstruktor salinan dengan =deletedi akhir.


Shallow vs Deep Copy

Ini adalah kasus yang paling dipahami dan sebenarnya satu-satunya yang disebutkan dalam jawaban lain. shaprtooth telah menutupinya dengan cukup baik. Saya hanya ingin menambahkan bahwa menyalin sumber daya yang harus dimiliki secara eksklusif oleh objek dapat berlaku untuk semua jenis sumber daya, di mana memori yang dialokasikan secara dinamis hanyalah satu jenis. Jika perlu, menyalin objek secara mendalam juga mungkin diperlukan

  • menyalin file sementara di disk
  • membuka koneksi jaringan terpisah
  • membuat utas pekerja terpisah
  • mengalokasikan framebuffer OpenGL terpisah
  • dll

Objek yang mendaftar sendiri

Pertimbangkan kelas di mana semua objek - tidak peduli bagaimana mereka telah dibangun - HARUS terdaftar. Beberapa contoh:

  • Contoh paling sederhana: mempertahankan jumlah total objek yang ada saat ini. Pendaftaran objek hanyalah tentang menaikkan penghitung statis.

  • Contoh yang lebih kompleks adalah memiliki registri tunggal, di mana referensi ke semua objek yang ada dari jenis tersebut disimpan (sehingga pemberitahuan dapat dikirimkan ke semuanya).

  • Pointer cerdas yang dihitung referensi dapat dianggap sebagai kasus khusus dalam kategori ini: penunjuk baru "mendaftar" sendiri dengan sumber daya bersama daripada di registri global.

Operasi pendaftaran sendiri seperti itu harus dilakukan oleh SETIAP konstruktor jenis dan konstruktor salinan tidak terkecuali.


Objek dengan referensi silang internal

Beberapa objek mungkin memiliki struktur internal non-trivial dengan referensi silang langsung antara sub-objek yang berbeda (pada kenyataannya, hanya satu referensi silang internal yang cukup untuk memicu kasus ini). Konstruktor salinan yang disediakan kompiler akan memutus asosiasi intra-objek internal , mengubahnya menjadi asosiasi antar-objek .

Sebuah contoh:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Hanya objek yang memenuhi kriteria tertentu yang diperbolehkan untuk disalin

Mungkin ada kelas di mana objek aman untuk disalin sementara dalam beberapa kondisi (misalnya default-built-state) dan tidak aman untuk menyalin sebaliknya. Jika kita ingin mengizinkan penyalinan objek safe-to-copy, maka - jika memprogram secara defensif - kita memerlukan pemeriksaan run-time di konstruktor salinan yang ditentukan pengguna.


Sub-objek yang tidak dapat disalin

Terkadang, kelas yang harus dapat disalin menggabungkan sub-objek yang tidak dapat disalin. Biasanya, hal ini terjadi untuk objek dengan status non-observasi (kasus tersebut dibahas lebih detail di bagian "Pengoptimalan" di bawah). Kompilator hanya membantu mengenali kasus itu.


Sub-objek yang bisa disalin semu

Sebuah kelas, yang harus dapat disalin, dapat menggabungkan sub-objek dari tipe yang dapat disalin. Tipe yang dapat disalin semu tidak menyediakan konstruktor salinan dalam arti yang sebenarnya, tetapi memiliki konstruktor lain yang memungkinkan untuk membuat salinan konseptual objek. Alasan untuk membuat sebuah tipe semantik dapat disalin adalah ketika tidak ada kesepakatan penuh tentang semantik salinan dari tipe tersebut.

Misalnya, meninjau kembali kasus pendaftaran diri objek, kita dapat berargumen bahwa mungkin ada situasi di mana sebuah objek harus didaftarkan dengan manajer objek global hanya jika itu adalah objek mandiri yang lengkap. Jika ia merupakan sub obyek dari obyek lain, maka tanggung jawab mengelolanya ada pada obyek yang memuatnya.

Atau, penyalinan dangkal dan dalam harus didukung (tidak ada yang menjadi default).

Kemudian keputusan akhir diserahkan kepada pengguna jenis itu - saat menyalin objek, mereka harus secara eksplisit menentukan (melalui argumen tambahan) metode penyalinan yang dimaksudkan.

Dalam kasus pendekatan non-defensif untuk pemrograman, mungkin juga ada konstruktor salinan biasa dan konstruktor kuasi-salinan. Hal ini dapat dibenarkan jika dalam sebagian besar kasus metode penyalinan tunggal harus diterapkan, sedangkan dalam situasi yang jarang tetapi dipahami dengan baik, metode penyalinan alternatif harus digunakan. Kemudian kompilator tidak akan mengeluh bahwa ia tidak dapat secara implisit mendefinisikan konstruktor salinan; itu akan menjadi tanggung jawab pengguna untuk mengingat dan memeriksa apakah sub-objek dari tipe itu harus disalin melalui konstruktor kuasi-copy.


Jangan salin status yang sangat terkait dengan identitas objek

Dalam kasus yang jarang terjadi, bagian dari keadaan objek yang dapat diamati dapat merupakan (atau dianggap) bagian yang tidak dapat dipisahkan dari identitas objek dan tidak boleh dipindahtangankan ke objek lain (meskipun ini bisa agak kontroversial).

Contoh:

  • UID objek (tetapi yang ini juga termasuk dalam kasus "pendaftaran sendiri" dari atas, karena id harus diperoleh dalam tindakan pendaftaran sendiri).

  • Sejarah objek (misalnya, Urungkan / Ulangi tumpukan) dalam kasus ketika objek baru tidak boleh mewarisi sejarah objek sumber, tetapi dimulai dengan satu item riwayat " Disalin pada <TIME> dari <OTHER_OBJECT_ID> ".

Dalam kasus seperti itu, konstruktor salinan harus melewati penyalinan sub-objek yang sesuai.


Menerapkan tanda tangan yang benar dari pembuat salinan

Tanda tangan dari konstruktor salinan yang disediakan kompiler bergantung pada konstruktor salinan apa yang tersedia untuk sub-objek. Jika setidaknya satu sub-objek tidak memiliki konstruktor salinan nyata (mengambil objek sumber dengan referensi konstan) tetapi memiliki konstruktor salinan yang bermutasi (mengambil objek sumber dengan referensi non-konstan) maka compiler tidak akan punya pilihan tetapi untuk secara implisit mendeklarasikan dan kemudian mendefinisikan konstruktor salinan yang bermutasi.

Sekarang, bagaimana jika copy-constructor yang "bermutasi" dari tipe sub-objek tidak benar-benar mengubah objek sumber (dan hanya ditulis oleh programmer yang tidak tahu tentang constkata kunci)? Jika kita tidak dapat memperbaiki kode itu dengan menambahkan yang hilang const, maka opsi lainnya adalah mendeklarasikan konstruktor salinan yang ditentukan pengguna kita sendiri dengan tanda tangan yang benar dan melakukan dosa beralih ke a const_cast.


Salin-saat-tulis (KK)

Kontainer COW yang telah memberikan referensi langsung ke data internalnya HARUS disalin secara mendalam pada saat pembuatan, jika tidak, kontainer tersebut dapat berperilaku sebagai pegangan penghitungan referensi.

Meskipun COW adalah teknik pengoptimalan, logika dalam konstruktor salinan ini sangat penting untuk implementasi yang benar. Itulah mengapa saya menempatkan kasus ini di sini daripada di bagian "Pengoptimalan", yang akan kita lanjutkan selanjutnya.



Optimasi

Dalam kasus berikut, Anda mungkin ingin / perlu menentukan konstruktor salinan Anda sendiri karena masalah pengoptimalan:


Pengoptimalan struktur selama penyalinan

Pertimbangkan wadah yang mendukung operasi penghapusan elemen, tetapi dapat melakukannya hanya dengan menandai elemen yang dihapus sebagai dihapus, dan mendaur ulang slotnya nanti. Saat salinan penampung seperti itu dibuat, mungkin masuk akal untuk memadatkan data yang masih ada daripada menyimpan slot yang "dihapus" sebagaimana adanya.


Lewati penyalinan status non-observasi

Sebuah objek mungkin berisi data yang bukan merupakan bagian dari statusnya yang dapat diamati. Biasanya, ini adalah data cache / memoized yang terakumulasi selama masa pakai objek untuk mempercepat operasi kueri lambat tertentu yang dilakukan oleh objek. Aman untuk melewati penyalinan data itu karena akan dihitung ulang ketika (dan jika!) Operasi yang relevan dilakukan. Menyalin data ini mungkin tidak dapat dibenarkan, karena dapat dengan cepat menjadi tidak valid jika status objek yang dapat diamati (dari mana data cache diturunkan) diubah dengan operasi mutasi (dan jika kita tidak akan memodifikasi objek, mengapa kita membuat salin kemudian?)

Pengoptimalan ini dibenarkan hanya jika data tambahannya besar dibandingkan dengan data yang mewakili status yang dapat diamati.


Nonaktifkan penyalinan implisit

C ++ memungkinkan untuk menonaktifkan penyalinan implisit dengan mendeklarasikan konstruktor salinan explicit. Kemudian objek dari kelas itu tidak bisa diteruskan ke fungsi dan / atau dikembalikan dari fungsi dengan nilai. Trik ini dapat digunakan untuk jenis yang tampak ringan tetapi memang sangat mahal untuk disalin (meskipun, membuatnya dapat disalin secara semu mungkin merupakan pilihan yang lebih baik).

Dalam C ++ 03 mendeklarasikan konstruktor salinan diperlukan juga untuk mendefinisikannya (tentu saja, jika Anda bermaksud menggunakannya). Oleh karena itu, pergi ke konstruktor salinan seperti itu hanya karena perhatian yang sedang dibahas berarti Anda harus menulis kode yang sama yang akan dihasilkan kompilator secara otomatis untuk Anda.

C ++ 11 dan standar yang lebih baru mengizinkan deklarasi fungsi anggota khusus (konstruktor default dan copy, operator penugasan salinan, dan destruktor) dengan permintaan eksplisit untuk menggunakan implementasi default (cukup akhiri deklarasi dengan =default).



TODO

Jawaban ini dapat diperbaiki sebagai berikut:

  • Tambahkan lebih banyak kode contoh
  • Gambarkan kasus "Objek dengan referensi silang internal"
  • Tambahkan beberapa tautan
Leon
sumber
6

Jika Anda memiliki kelas yang memiliki konten yang dialokasikan secara dinamis. Misalnya Anda menyimpan judul buku sebagai karakter * dan menyetel judul dengan baru, penyalinan tidak akan berfungsi.

Anda harus menulis konstruktor salinan yang melakukannya title = new char[length+1]dan kemudian strcpy(title, titleIn). Pembuat salinan hanya akan melakukan penyalinan "dangkal".

Peter Ajtai
sumber
2

Copy Constructor dipanggil ketika sebuah objek diteruskan oleh nilai, dikembalikan oleh nilai, atau disalin secara eksplisit. Jika tidak ada konstruktor salinan, c ++ membuat konstruktor salinan default yang membuat salinan dangkal. Jika objek tidak memiliki pointer ke memori yang dialokasikan secara dinamis maka salinan dangkal akan dilakukan.

astaga
sumber
0

Seringkali merupakan ide yang baik untuk menonaktifkan copy ctor, dan operator = kecuali kelas secara khusus membutuhkannya. Ini dapat mencegah inefisiensi seperti meneruskan argumen dengan nilai saat referensi dimaksudkan. Juga metode yang dihasilkan kompilator mungkin tidak valid.

seand
sumber
-1

Mari pertimbangkan cuplikan kode di bawah ini:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();memberikan keluaran sampah karena ada konstruktor salinan yang ditentukan pengguna yang dibuat tanpa kode yang ditulis untuk menyalin data secara eksplisit. Jadi compiler tidak membuat yang sama.

Berpikirlah untuk berbagi pengetahuan ini dengan Anda semua, meskipun sebagian besar dari Anda sudah mengetahuinya.

Cheers ... Selamat membuat kode !!!

Pankaj Kumar Thapa
sumber