Saya percaya saya telah mencari berkali-kali tentang penghancur virtual, kebanyakan menyebutkan tujuan penghancur virtual, dan mengapa Anda perlu penghancur virtual. Juga saya pikir dalam banyak kasus destruktor harus virtual.
Maka pertanyaannya adalah: Mengapa c ++ tidak mengatur semua destruktor virtual secara default? atau dalam pertanyaan lain:
Kapan saya TIDAK perlu menggunakan destruktor virtual?
Dalam hal mana saya TIDAK boleh menggunakan destruktor virtual?
Berapa biaya menggunakan destruktor virtual jika saya menggunakannya bahkan jika itu tidak diperlukan?
c++
virtual-functions
ggrr
sumber
sumber
Jawaban:
Jika Anda menambahkan destruktor virtual ke kelas:
di sebagian besar (semua?) implementasi C ++ saat ini, setiap instance objek dari kelas itu perlu menyimpan pointer ke tabel pengiriman virtual untuk tipe runtime, dan tabel pengiriman virtual itu sendiri ditambahkan ke gambar yang dapat dieksekusi
alamat tabel pengiriman virtual belum tentu valid di seluruh proses, yang dapat mencegah berbagi objek dengan aman dalam memori bersama
memiliki pointer virtual tertanam membuat frustasi membuat kelas dengan tata letak memori yang cocok dengan beberapa format input atau output yang diketahui (misalnya, sehingga
Price_Tick*
dapat diarahkan langsung pada memori yang selaras dalam paket UDP yang masuk dan digunakan untuk mengurai / mengakses atau mengubah data, atau menempatkannew
kelas semacam itu untuk menulis data ke dalam paket keluar)destructor menyebut diri mereka mungkin - dalam kondisi tertentu - harus dikirim secara virtual dan oleh karena itu out-of-line, sedangkan destruktor non-virtual mungkin sedang digariskan atau dioptimalkan jauh jika sepele atau tidak relevan dengan penelepon
Argumen "tidak dirancang untuk diwarisi dari" tidak akan menjadi alasan praktis untuk tidak selalu memiliki destruktor virtual jika tidak juga lebih buruk dengan cara praktis seperti yang dijelaskan di atas; tetapi mengingat itu lebih buruk itu adalah kriteria utama kapan harus membayar biaya: standar untuk memiliki destruktor virtual jika kelas Anda dimaksudkan untuk digunakan sebagai kelas dasar . Itu tidak selalu diperlukan, tetapi memastikan kelas-kelas dalam heirarki dapat digunakan lebih bebas tanpa perilaku tidak sengaja yang tidak disengaja jika destruktor kelas turunan dipanggil menggunakan pointer kelas dasar atau referensi.
Tidak begitu ... banyak kelas tidak membutuhkannya. Ada begitu banyak contoh di mana tidak perlu rasanya konyol untuk menyebutkannya, tetapi cukup telusuri Perpustakaan Standar Anda atau katakan peningkatan dan Anda akan melihat ada sebagian besar kelas yang tidak memiliki penghancur virtual. Dalam meningkatkan 1,53 saya menghitung 72 destruktor virtual dari 494.
sumber
BTW,
Untuk kelas dasar dengan penghapusan polimorfik.
sumber
Biaya memperkenalkan fungsi virtual apa pun ke kelas (diwarisi atau bagian dari definisi kelas) adalah biaya awal yang mungkin sangat curam (atau tidak tergantung pada objek) dari pointer virtual yang disimpan per objek, seperti:
Dalam hal ini, biaya memori relatif besar. Ukuran memori aktual dari instance kelas sekarang akan sering terlihat seperti ini pada arsitektur 64-bit:
Totalnya adalah 16 byte untuk
Integer
kelas ini dibandingkan dengan hanya 4 byte. Jika kita menyimpan sejuta dari ini dalam sebuah array, kita berakhir dengan 16 megabyte penggunaan memori: dua kali ukuran cache CPU L3 8 MB, dan iterasi melalui array seperti itu berulang kali bisa berkali-kali lebih lambat daripada setara 4 megabyte tanpa penunjuk virtual sebagai akibat dari kesalahan cache tambahan dan kesalahan halaman.Namun, biaya penunjuk virtual ini per objek, tidak meningkat dengan lebih banyak fungsi virtual. Anda dapat memiliki 100 fungsi anggota virtual di kelas dan overhead per instance masih akan menjadi pointer virtual tunggal.
Pointer virtual biasanya menjadi perhatian yang lebih langsung dari sudut pandang overhead. Namun, selain pointer virtual per instance adalah biaya per kelas. Setiap kelas dengan fungsi virtual menghasilkan
vtable
dalam memori yang menyimpan alamat ke fungsi yang seharusnya dipanggil (pengiriman virtual / dinamis) ketika panggilan fungsi virtual dibuat. Thevptr
disimpan per contoh maka poin untuk ini kelas khususvtable
. Overhead ini biasanya menjadi masalah yang lebih kecil, tetapi mungkin mengembang ukuran biner Anda dan menambahkan sedikit biaya runtime jika overhead ini dibayar sia-sia untuk seribu kelas dalam basis kode yang kompleks, misalnyavtable
sisi biaya ini sebenarnya meningkat secara proporsional dengan lebih dan lebih lebih banyak fungsi virtual dalam campuran.Pengembang Java yang bekerja di area kritis kinerja memahami jenis overhead ini dengan sangat baik (meskipun sering dijelaskan dalam konteks tinju), karena tipe Java yang didefinisikan pengguna secara inherit mewarisi dari
object
kelas dasar pusat dan semua fungsi di Jawa secara implisit virtual (dapat ditimpa) ) di alam kecuali ditandai sebaliknya. Akibatnya, JavaInteger
juga cenderung membutuhkan 16 byte memori pada platform 64-bit sebagai hasil darivptr
metadata gaya yang terkait per instance, dan itu biasanya tidak mungkin di Jawa untuk membungkus sesuatu seperti singleint
ke dalam kelas tanpa membayar runtime biaya kinerja untuk itu.C ++ benar-benar mendukung kinerja dengan pola pikir "pay as you go" dan juga masih banyak desain yang digerakkan oleh perangkat keras berbasis logam yang diwarisi dari C. Tidak perlu menyertakan biaya overhead yang diperlukan untuk pembuatan vtable dan pengiriman dinamis untuk setiap kelas / instance yang terlibat. Jika kinerja bukan salah satu alasan utama Anda menggunakan bahasa seperti C ++, Anda mungkin mendapat manfaat lebih dari bahasa pemrograman lain di luar sana karena banyak bahasa C ++ kurang aman dan lebih sulit daripada idealnya dengan kinerja yang sering alasan utama untuk menyukai desain seperti itu.
Cukup sering. Jika suatu kelas tidak dirancang untuk diwariskan, maka ia tidak memerlukan destruktor virtual dan hanya akan membayar biaya overhead yang besar untuk sesuatu yang tidak perlu. Demikian juga, bahkan jika suatu kelas dirancang untuk diwariskan tetapi Anda tidak pernah menghapus instance subtipe melalui basis pointer, maka itu juga tidak memerlukan destruktor virtual. Dalam hal itu, praktik yang aman adalah menentukan destruktor nonvirtual yang dilindungi, seperti:
Sebenarnya lebih mudah untuk ditutup ketika Anda harus menggunakan destruktor virtual. Kelas yang jauh lebih sering dalam basis kode Anda tidak akan dirancang untuk warisan.
std::vector
, misalnya, tidak dirancang untuk diwarisi dan biasanya tidak boleh diwarisi (desain yang sangat goyah), karena hal itu kemudian akan rentan terhadap masalah penghapusan penunjuk basis ini (std::vector
sengaja menghindari destruktor virtual) selain masalah kikuk objek yang kikuk jika Anda kelas turunan menambahkan status baru apa pun.Secara umum kelas yang diwarisi harus memiliki destruktor virtual publik atau yang dilindungi, nonvirtual. Dari
C++ Coding Standards
, bab 50:Salah satu hal yang C ++ cenderung agak menekankan secara implisit (karena desain cenderung menjadi sangat rapuh dan canggung dan mungkin bahkan tidak aman sebaliknya) adalah gagasan bahwa pewarisan bukan merupakan mekanisme yang dirancang untuk digunakan sebagai renungan. Ini merupakan mekanisme yang dapat diperluas dengan polimorfisme, tetapi yang membutuhkan tinjauan ke depan tentang ke mana kemungkinan diperpanjang. Akibatnya, kelas dasar Anda harus dirancang sebagai akar hierarki warisan di muka, dan bukan sesuatu yang Anda warisi dari nanti sebagai renungan tanpa pandangan jauh ke depan seperti sebelumnya.
Dalam kasus-kasus di mana Anda hanya ingin mewarisi untuk menggunakan kembali kode yang ada, komposisi sering sangat dianjurkan (Prinsip Penggunaan Kembali Komposit).
sumber
Mengapa c ++ tidak mengatur semua destruktor virtual secara default? Biaya penyimpanan tambahan dan panggilan tabel metode virtual. C ++ digunakan untuk sistem, latensi rendah, pemrograman rt di mana ini bisa menjadi beban.
sumber
Ini adalah contoh yang baik kapan tidak menggunakan destruktor virtual: Dari Scott Meyers:
Jika kelas tidak mengandung fungsi virtual, itu sering merupakan indikasi bahwa itu tidak dimaksudkan untuk digunakan sebagai kelas dasar. Ketika sebuah kelas tidak dimaksudkan untuk digunakan sebagai kelas dasar, membuat virtual destructor biasanya merupakan ide yang buruk. Pertimbangkan contoh ini, berdasarkan pada diskusi di ARM:
Jika sebuah int pendek menempati 16 bit, sebuah objek Point dapat masuk ke dalam register 32-bit. Selain itu, objek Point dapat diteruskan sebagai kuantitas 32-bit ke fungsi yang ditulis dalam bahasa lain seperti C atau FORTRAN. Namun, jika destruktor Point dibuat virtual, situasinya berubah.
Saat Anda menambahkan anggota virtual, pointer virtual ditambahkan ke kelas Anda yang menunjuk ke tabel virtual untuk kelas itu.
sumber
If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.
Wut. Apakah ada orang lain yang mengingat Hari-Hari Tua yang Baik, di mana kami diizinkan menggunakan kelas dan warisan untuk membangun lapisan berturut-turut dari anggota dan perilaku yang dapat digunakan kembali, tanpa harus peduli dengan metode virtual sama sekali? Ayo, Scott. Saya mendapatkan poin intinya, tetapi itu "sering" benar-benar mencapai.Destructor virtual menambahkan biaya runtime. Biayanya sangat besar jika kelas tidak memiliki metode virtual lainnya. Destructor virtual juga hanya diperlukan dalam satu skenario tertentu, di mana objek dihapus atau dihancurkan melalui pointer ke kelas dasar. Dalam hal ini, destruktor kelas dasar harus virtual, dan destruktor dari setiap kelas turunan akan virtual secara implisit. Ada beberapa skenario di mana kelas dasar polimorfik digunakan sedemikian rupa sehingga destruktor tidak perlu virtual:
std::unique_ptr<Derived>
, dan polimorfisme terjadi hanya melalui pointer dan referensi yang tidak memiliki. Contoh lain adalah ketika objek dialokasikan menggunakanstd::make_shared<Derived>()
. Tidak apa-apa untuk digunakanstd::shared_ptr<Base>
dan selama pointer awal adalah astd::shared_ptr<Derived>
. Ini karena pointer bersama memiliki pengiriman dinamis mereka sendiri untuk destruktor (deleter) yang tidak selalu bergantung pada destruktor kelas dasar virtual.Tentu saja, setiap konvensi untuk menggunakan objek hanya dengan cara yang disebutkan di atas dapat dengan mudah dipatahkan. Oleh karena itu, saran Herb Sutter tetap valid seperti sebelumnya: "Penghancur kelas dasar harus bersifat publik dan virtual, atau dilindungi dan non-virtual." Dengan begitu, jika seseorang mencoba menghapus pointer ke kelas dasar dengan destruktor non-virtual, ia kemungkinan besar akan menerima kesalahan pelanggaran akses pada waktu kompilasi.
Kemudian lagi ada kelas yang tidak dirancang untuk menjadi kelas dasar (publik). Rekomendasi pribadi saya adalah membuatnya
final
dalam C ++ 11 atau lebih tinggi. Jika itu dirancang untuk menjadi pasak persegi, maka kemungkinan itu tidak akan bekerja dengan baik seperti pasak bulat. Ini terkait dengan preferensi saya untuk memiliki kontrak warisan eksplisit antara kelas dasar dan kelas turunan, untuk pola desain NVI (antarmuka non-virtual), untuk kelas dasar abstrak dan bukan beton, dan kebencian saya pada variabel anggota terlindungi, antara lain , tapi saya tahu semua pandangan ini kontroversial sampai taraf tertentu.sumber
Mendeklarasikan sebuah destruktor
virtual
hanya diperlukan ketika Anda berencana untuk membuatclass
warisan Anda . Biasanya kelas-kelas perpustakaan standar (sepertistd::string
) tidak menyediakan destruktor virtual dan dengan demikian tidak dimaksudkan untuk subkelas.sumber
delete
pointer ke kelas dasar.Akan ada overhead dalam konstruktor untuk membuat vtable (jika Anda tidak memiliki fungsi virtual lain, dalam hal ini Anda MUNGKIN, tetapi tidak selalu, harus memiliki destruktor virtual juga). Dan jika Anda tidak memiliki fungsi virtual lain, itu membuat objek Anda satu ukuran pointer lebih besar dari yang diperlukan. Jelas, peningkatan ukuran dapat berdampak besar pada benda-benda kecil.
Ada memori tambahan yang dibaca untuk mendapatkan vtable dan kemudian memanggil fungsi tidak langsung melalui itu, yang overhead daripada destruktor non-virtual ketika destructor dipanggil. Dan tentu saja, sebagai konsekuensinya, sedikit kode tambahan yang dihasilkan untuk setiap panggilan ke destruktor. Ini untuk kasus-kasus di mana kompiler tidak dapat menyimpulkan tipe sebenarnya - dalam kasus-kasus di mana ia dapat menyimpulkan tipe aktual, kompiler tidak akan menggunakan vtable, tetapi memanggil destruktor secara langsung.
Anda harus memiliki destruktor virtual jika kelas Anda dimaksudkan sebagai kelas-dasar, khususnya jika dapat dibuat / dihancurkan oleh beberapa entitas selain kode yang tahu jenis apa yang dibuat, maka Anda memerlukan destruktor virtual.
Jika Anda tidak yakin, gunakan destruktor virtual. Lebih mudah untuk menghapus virtual jika muncul sebagai masalah daripada mencoba menemukan bug yang disebabkan oleh "destruktor yang tepat tidak disebut".
Singkatnya, Anda seharusnya tidak memiliki destruktor virtual jika: 1. Anda tidak memiliki fungsi virtual. 2. Jangan berasal dari kelas (tandai
final
di C ++ 11, dengan cara itu kompiler akan memberi tahu jika Anda mencoba untuk menurunkannya).Dalam kebanyakan kasus, pembuatan dan penghancuran bukanlah bagian utama dari waktu yang dihabiskan menggunakan objek tertentu kecuali ada "banyak konten" (membuat string 1MB jelas akan memakan waktu, karena setidaknya 1MB data perlu disalin dari manapun lokasinya saat ini). Menghancurkan string 1MB tidak lebih buruk daripada penghancuran string 150B, keduanya akan memerlukan deallocating penyimpanan string, dan tidak banyak lagi, sehingga waktu yang dihabiskan di sana biasanya sama [kecuali itu membangun debug, di mana deallokasi sering mengisi memori dengan sebuah "pola racun" - tetapi itu bukan bagaimana Anda akan menjalankan aplikasi nyata Anda dalam produksi].
Singkatnya, ada overhead kecil, tetapi untuk objek kecil, itu mungkin membuat perbedaan.
Perhatikan juga bahwa kompiler dapat mengoptimalkan pencarian virtual dalam beberapa kasus, jadi itu hanya penalti
Seperti biasa dalam hal kinerja, jejak memori, dan semacamnya: Tolok ukur dan profil dan ukur, bandingkan hasilnya dengan alternatif, dan lihat di mana PALING waktu / memori dihabiskan, dan jangan mencoba mengoptimalkan 90% dari kode yang tidak berjalan banyak [sebagian besar aplikasi memiliki sekitar 10% dari kode yang sangat berpengaruh pada waktu eksekusi, dan 90% dari kode yang tidak memiliki banyak pengaruh sama sekali]. Lakukan ini dalam level optimisasi tinggi, sehingga Anda sudah mendapatkan manfaat dari kompiler melakukan pekerjaan dengan baik! Dan ulangi, periksa lagi dan tingkatkan secara bertahap. Jangan mencoba menjadi pintar dan mencari tahu apa yang penting dan apa yang tidak, kecuali jika Anda memiliki banyak pengalaman dengan jenis aplikasi tertentu.
sumber
You **should** have a virtual destructor if your class is intended as a base-class
terlalu menyederhanakan - dan pesimis prematur . Ini hanya diperlukan jika ada yang diizinkan untuk menghapus kelas turunan melalui pointer ke basis. Dalam banyak situasi, tidak demikian halnya. Jika Anda tahu itu, maka pasti, mengeluarkan biaya overhead. Yang, btw, selalu ditambahkan, bahkan jika panggilan aktual dapat diselesaikan secara statis oleh kompiler. Kalau tidak, ketika Anda benar mengontrol apa yang dapat dilakukan orang dengan benda Anda, itu tidak bermanfaat