Abstrak kelas dasar dan copy konstruksi, aturan praktis

10

Sering kali itu ide yang baik untuk memiliki kelas dasar abstrak untuk mengisolasi antarmuka objek.

Masalahnya adalah bahwa konstruksi salinan, IMHO, cukup banyak rusak secara default di C ++, dengan copy constructor yang dihasilkan secara default.

Jadi, apa sajakah Gotcha ketika Anda memiliki kelas dasar abstrak dan petunjuk mentah di kelas turunan?

class IAbstract
{
    ~IAbstract() = 0;
}

class Derived : public IAbstract
{
    char *theProblem;
    ...
}

IAbstract *a1 = new Derived();
IAbstract a2 = *a1;//???

Dan sekarang apakah Anda dengan bersih menonaktifkan konstruksi salinan untuk seluruh hierarki? Mengumumkan konstruksi salinan sebagai pribadi di IAbstract?

Apakah ada aturan tiga dengan kelas dasar abstrak?

Coder
sumber
1
gunakan referensi alih-alih petunjuk :)
tp1
@ tp1: atau setidaknya penunjuk pintar.
Benjamin Bannier
1
Terkadang Anda hanya perlu bekerja dengan kode yang ada ... Anda tidak dapat mengubah semuanya dalam sekejap.
Coder
Menurut Anda mengapa konstruktor salinan default rusak?
BЈовић
2
@Coder: Panduan gaya Google adalah tumpukan sampah dan benar-benar menyebalkan untuk setiap pengembangan C ++.
DeadMG

Jawaban:

6

Salinan konstruksi pada kelas abstrak harus dibuat pribadi dalam banyak kasus, serta operator penugasan.

Kelas abstrak, menurut definisi, dibuat menjadi tipe polimorfik. Jadi, Anda tidak tahu berapa banyak memori yang digunakan oleh instance Anda, sehingga tidak dapat menyalin atau menetapkannya dengan aman. Dalam praktiknya, Anda berisiko mengiris: /programming/274626/what-is-the-slicing-problem-in-c

Jenis polimorfik, dalam C ++, tidak boleh dimanipulasi oleh nilai. Anda memanipulasi mereka dengan referensi atau dengan pointer (atau smart pointer).

Ini adalah alasan mengapa Java membuat objek dapat dimanipulasi hanya dengan referensi, dan mengapa C # dan D memiliki pemisahan antara kelas dan struct (yang pertama adalah tipe polimorfik dan referensi, yang kedua adalah non polimorfik dan tipe nilai).

deadalnix
sumber
2
Hal-hal di Jawa tidak lebih baik. Di Jawa itu menyakitkan untuk menyalin apa pun, bahkan ketika Anda benar-benar perlu, dan sangat mudah lupa melakukannya. Jadi Anda berakhir dengan bug jahat di mana dua struktur data memiliki referensi ke kelas nilai yang sama (misalnya Tanggal), dan kemudian salah satunya mengubah Tanggal dan struktur data lainnya sekarang rusak.
kevin cline
3
Ah. Ini bukan masalah - siapa pun yang tahu C ++ tidak akan membuat kesalahan ini. Lebih penting lagi, tidak ada alasan sama sekali untuk memanipulasi tipe polimorfik berdasarkan nilai jika Anda tahu apa yang Anda lakukan. Anda memulai reaksi brengsek total atas kemungkinan yang secara teknis tipis. Pointer NULL adalah masalah yang lebih besar.
DeadMG
8

Anda bisa, tentu saja, membuatnya terlindungi dan kosong, sehingga kelas turunan dapat memilih. Namun, lebih umum, kode Anda dilarang pula karena tidak mungkin untuk instantiate IAbstract- karena memiliki fungsi virtual murni. Dengan demikian, ini umumnya bukan masalah - kelas antarmuka Anda tidak dapat dipakai dan oleh karena itu tidak dapat disalin, dan kelas Anda yang lebih diturunkan dapat melarang atau terus menyalin seperti yang mereka inginkan.

DeadMG
sumber
Dan bagaimana jika ada abstrak -> Derived1 -> Derived2 hierarki, dan Derived2 ditugaskan ke Derived1? Sudut-sudut gelap bahasa ini masih mengejutkan saya.
Coder
@Coder: Itu biasanya dipandang sebagai PEBKAC. Namun, lebih langsung, Anda selalu dapat mendeklarasikan sebagai pribadi operator=(const Derived2&)di Derived1.
DeadMG
Ok, mungkin ini benar-benar bukan masalah. Saya hanya ingin memastikan kelas aman terhadap pelecehan.
Coder
2
Dan untuk itu kamu harus mendapatkan medali.
Insinyur Dunia
2
@ Kode: Pelanggaran oleh siapa? Yang terbaik yang dapat Anda lakukan adalah membuatnya mudah untuk menulis kode yang benar. Tidak ada gunanya mencoba bertahan melawan programmer yang tidak tahu C ++.
kevin cline
2

Dengan membuat ctor dan tugas pribadi (atau dengan mendeklarasikannya sebagai = delete di C ++ 11) Anda menonaktifkan salinan.

Intinya di sini adalah DI MANA Anda harus melakukan itu. Untuk tetap menggunakan kode Anda, IAbstract tidak menjadi masalah. (perhatikan bahwa melakukan apa yang Anda lakukan, Anda menetapkan *a1 IAbstractsubobjek ke a2, kehilangan referensi apa pun Derived. Penugasan nilai bukan polimorfik)

Masalahnya datang dengan Derived::theproblem. Menyalin Turunan ke yang lain mungkin sebenarnya berbagi *theproblemdata yang mungkin tidak dirancang untuk dibagikan (ada dua contoh yang dapat memanggil delete theproblemdestruktor mereka).

Jika itu masalahnya, itu Derivedharus tidak dapat disalin dan tidak dapat dialihkan. Tentu saja, jika Anda membuat salinan pribadi dalam IAbstract, karena salinan default untuk Derivedkebutuhan itu, Derivedjuga tidak akan dapat disalin. Tetapi jika Anda mendefinisikan sendiri Derived::Derived(const Derived&)tanpa memanggil IAbtractsalinan, Anda masih dapat menyalinnya.

Masalahnya adalah dalam Derived, dan solusinya harus tetap ke Derived: jika itu harus objek dinamis yang hanya diakses oleh pointer atau referensi, itu adalah Derived itu sendiri yang harus memiliki

class Derived
{
    ...
    Derived(const Derived&) = delete;
    Derived& operator=(const Derived&) = delete;
};

Pada dasarnya tergantung pada perancang kelas Derived (yang seharusnya tahu bagaimana Derived bekerja dan bagaimana theproblemdikelola) untuk memutuskan apa yang harus dilakukan dengan penugasan dan salinan.

Emilio Garavaglia
sumber