Kapan BUKAN menggunakan destruktor virtual?

48

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?

ggrr
sumber
6
Dan bagaimana jika kelas Anda tidak seharusnya diwariskan? Lihatlah banyak kelas perpustakaan standar, beberapa memiliki fungsi virtual karena mereka tidak dirancang untuk diwariskan.
Beberapa programmer Bung
4
Juga saya pikir dalam banyak kasus destruktor harus virtual. Nggak. Tidak semuanya. Hanya mereka yang menyalahgunakan warisan (daripada mendukung komposisi) yang berpikir demikian. Saya telah melihat seluruh aplikasi dengan hanya beberapa kelas dasar dan fungsi virtual.
Matthieu M.
1
@underscore_d Dengan implementasi tipikal, akan ada kode tambahan yang dihasilkan untuk kelas polimorfik apa pun kecuali semua hal tersirat telah didevirtualisasi dan dioptimalkan. Dalam ABI umum, ini melibatkan setidaknya satu tabel untuk setiap kelas. Tata letak kelas juga harus diubah. Anda tidak dapat kembali dengan andal begitu Anda telah menerbitkan kelas semacam itu sebagai bagian dari beberapa antarmuka publik, karena mengubahnya lagi akan merusak kompatibilitas ABI, karena jelas buruk (jika mungkin) mengharapkan devirtualization sebagai kontrak antarmuka secara umum.
FrankHB
1
@underscore_d Ungkapan "pada waktu kompilasi" tidak akurat, tapi saya pikir ini berarti destruktor virtual tidak bisa sepele atau dengan constexpr ditentukan, sehingga pembuatan kode tambahan sulit untuk dihindari (kecuali jika Anda benar-benar menghindari penghancuran objek semacam itu sama sekali) jadi itu akan lebih atau kurang membahayakan kinerja runtime.
FrankHB
2
@underscore_d "Pointer" tampak seperti herring merah. Mungkin harus menjadi pointer ke anggota (yang bukan merupakan pointer menurut definisi). Dengan ABI biasa, pointer ke anggota sering tidak cocok dengan kata mesin (seperti pointer khas), dan mengubah kelas dari non-polimorfik ke polimorfik sering akan mengubah ukuran pointer ke anggota kelas ini.
FrankHB

Jawaban:

41

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 menempatkan newkelas 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.

"dalam banyak kasus, destruktor harus virtual"

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.

Tony
sumber
23

Dalam hal mana saya TIDAK boleh menggunakan destruktor virtual?

  1. Untuk kelas konkret yang tidak ingin diwariskan.
  2. Untuk kelas dasar tanpa penghapusan polimorfik. Klien mana pun seharusnya tidak dapat menghapus secara polimorfik menggunakan pointer ke Base.

BTW,

Dalam hal mana harus menggunakan destruktor virtual?

Untuk kelas dasar dengan penghapusan polimorfik.

songyuanyao
sumber
7
+1 untuk # 2, khususnya tanpa penghapusan polimorfik . Jika destruktor Anda tidak pernah dapat dipanggil melalui basis pointer, membuatnya virtual tidak perlu dan berlebihan, terutama jika kelas Anda bukan virtual sebelumnya (sehingga menjadi baru membengkak dengan RTTI). Untuk menjaga dari setiap pengguna yang melanggar ini, seperti yang disarankan oleh Herb Sutter, Anda akan membuat dtor kelas dasar dilindungi dan non-virtual, sehingga hanya dapat dipanggil oleh / setelah destruktor yang diturunkan.
underscore_d
@underscore_d imho bahwa poin penting yang saya lewatkan dalam jawaban, seperti di hadapan warisan satu-satunya kasus di mana saya tidak memerlukan konstruktor virtual adalah ketika saya dapat memastikan bahwa itu tidak pernah diperlukan
sebelumnya dikenal
14

Berapa biaya menggunakan destruktor virtual jika saya menggunakannya bahkan jika itu tidak diperlukan?

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:

struct Integer
{
    virtual ~Integer() {}
    int value;
};

Dalam hal ini, biaya memori relatif besar. Ukuran memori aktual dari instance kelas sekarang akan sering terlihat seperti ini pada arsitektur 64-bit:

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

Totalnya adalah 16 byte untuk Integerkelas 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 vtabledalam memori yang menyimpan alamat ke fungsi yang seharusnya dipanggil (pengiriman virtual / dinamis) ketika panggilan fungsi virtual dibuat. The vptrdisimpan per contoh maka poin untuk ini kelas khusus vtable. 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, misalnya vtablesisi 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 objectkelas dasar pusat dan semua fungsi di Jawa secara implisit virtual (dapat ditimpa) ) di alam kecuali ditandai sebaliknya. Akibatnya, Java Integerjuga cenderung membutuhkan 16 byte memori pada platform 64-bit sebagai hasil dari vptrmetadata gaya yang terkait per instance, dan itu biasanya tidak mungkin di Jawa untuk membungkus sesuatu seperti single intke dalam kelas tanpa membayar runtime biaya kinerja untuk itu.

Maka pertanyaannya adalah: Mengapa c ++ tidak mengatur semua destruktor virtual secara default?

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.

Kapan saya TIDAK perlu menggunakan destruktor virtual?

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:

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

Dalam hal mana saya TIDAK boleh menggunakan destruktor virtual?

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::vectorsengaja 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:

50. Jadikan kelas dasar destruktor publik dan virtual, atau dilindungi dan nonvirtual. Untuk menghapus, atau tidak menghapus; itulah pertanyaannya: Jika penghapusan melalui pointer ke basis Base harus diizinkan, maka destruktor Base harus bersifat publik dan virtual. Kalau tidak, itu harus dilindungi dan nonvirtual.

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

ChrisF
sumber
9

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.

ML
sumber
Destructors tidak boleh digunakan di tempat pertama dalam sistem waktu nyata yang sulit karena banyak sumber daya seperti memori dinamis tidak dapat digunakan untuk memberikan jaminan tenggat waktu yang kuat
Marco A.
9
@MarcoA. Sejak kapan destruktor menyiratkan alokasi memori dinamis?
chbaker0
@ chbaker0 Saya menggunakan 'like'. Mereka tidak digunakan dalam pengalaman saya.
Marco A.
6
Juga tidak masuk akal bahwa memori dinamis tidak dapat digunakan dalam sistem waktu nyata yang sulit. Cukup sepele untuk membuktikan bahwa tumpukan yang telah dikonfigurasikan sebelumnya dengan ukuran alokasi tetap dan bitmap alokasi akan mengalokasikan memori atau mengembalikan kondisi kehabisan memori dalam waktu yang diperlukan untuk memindai bitmap itu.
MSalters
@ msalters yang membuat saya berpikir: bayangkan sebuah program di mana biaya setiap operasi disimpan dalam sistem tipe. Mengizinkan pemeriksaan kompilasi jaminan realtime.
Yakk
5

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:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

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.
underscore_d
3

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:

  • Jika instance kelas turunan tidak dialokasikan pada heap, misalnya hanya langsung di stack atau di dalam objek lain. (Kecuali jika Anda menggunakan memori yang tidak diinisialisasi dan operator penempatan yang baru.)
  • Jika instance kelas turunan dialokasikan pada heap, tetapi penghapusan hanya terjadi melalui pointer ke kelas yang paling diturunkan, misalnya ada std::unique_ptr<Derived>, dan polimorfisme terjadi hanya melalui pointer dan referensi yang tidak memiliki. Contoh lain adalah ketika objek dialokasikan menggunakan std::make_shared<Derived>(). Tidak apa-apa untuk digunakan std::shared_ptr<Base>dan selama pointer awal adalah a std::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 finaldalam 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.

Arne Vogel
sumber
1

Mendeklarasikan sebuah destruktor virtualhanya diperlukan ketika Anda berencana untuk membuat classwarisan Anda . Biasanya kelas-kelas perpustakaan standar (seperti std::string) tidak menyediakan destruktor virtual dan dengan demikian tidak dimaksudkan untuk subkelas.

Constantinius
sumber
3
Alasannya adalah subclassing + penggunaan polimorfisme. Destructor virtual diperlukan hanya jika resolusi dinamis diperlukan, yaitu referensi / pointer / apa pun ke kelas master sebenarnya bisa merujuk ke turunan dari subkelas.
Michel Billaud
2
@MichelBillaud sebenarnya Anda masih dapat memiliki polimorfisme tanpa dtor virtual. Dtor virtual HANYA diperlukan untuk menghapus polimorfik, yaitu memanggil deletepointer ke kelas dasar.
chbaker0
1

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

Mats Petersson
sumber
1
"akan menjadi overhead dalam konstruktor untuk membuat vtable" - vtable biasanya "dibuat" pada basis per-kelas oleh kompiler, dengan konstruktor hanya memiliki overhead untuk menyimpan pointer ke dalam instance objek yang sedang dibangun.
Tony
Selain itu ... Saya semua tentang menghindari optimasi prematur, tetapi sebaliknya, You **should** have a virtual destructor if your class is intended as a base-classterlalu 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
underscore_d