Kapan Anda tidak boleh menggunakan penghancur virtual?

99

Adakah alasan bagus untuk tidak mendeklarasikan destruktor virtual untuk kelas? Kapan sebaiknya Anda secara khusus menghindari menulisnya?

Mag Roader
sumber

Jawaban:

72

Tidak perlu menggunakan destruktor virtual jika salah satu hal di bawah ini benar:

  • Tidak ada niat untuk mendapatkan kelas darinya
  • Tidak ada contoh di heap
  • Tidak ada niat untuk menyimpan pointer dari superclass

Tidak ada alasan khusus untuk menghindarinya kecuali jika Anda benar-benar terdesak untuk mengingat.

sep
sumber
25
Ini bukan jawaban yang bagus. "Tidak perlu" berbeda dari "tidak boleh", dan "tidak ada niat" berbeda dari "dibuat tidak mungkin".
Pemrogram Windows
5
Juga tambahkan: tidak ada niat untuk menghapus sebuah instance melalui pointer kelas dasar.
Adam Rosenfield
9
Ini tidak benar-benar menjawab pertanyaan itu. Di mana alasan bagus Anda untuk tidak menggunakan dtor virtual?
mxcl
9
Saya pikir ketika tidak ada kebutuhan untuk melakukan sesuatu, itu adalah alasan yang baik untuk tidak melakukannya. Ini mengikuti prinsip Simple Design XP.
sep
12
Dengan mengatakan Anda "tidak berniat", Anda membuat asumsi besar tentang bagaimana kelas Anda akan terbiasa. Bagi saya solusi paling sederhana dalam banyak kasus (yang seharusnya menjadi default) harus memiliki penghancur virtual, dan hanya menghindarinya jika Anda memiliki alasan khusus untuk tidak melakukannya. Jadi saya masih penasaran tentang apa alasan yang bagus.
ckarras
68

Untuk menjawab pertanyaan secara eksplisit, yaitu kapan sebaiknya Anda tidak mendeklarasikan destruktor virtual.

C ++ '98 / '03

Menambahkan destruktor virtual dapat mengubah kelas Anda dari POD (data lama biasa) * atau digabungkan menjadi non-POD. Ini dapat menghentikan proyek Anda dari kompilasi jika jenis kelas Anda adalah agregat yang diinisialisasi di suatu tempat.

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

Dalam kasus ekstrim, perubahan seperti itu juga dapat menyebabkan perilaku tidak terdefinisi di mana kelas sedang digunakan dengan cara yang membutuhkan POD, misalnya meneruskannya melalui parameter elipsis, atau menggunakannya dengan memcpy.

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* Jenis POD adalah jenis yang memiliki jaminan khusus tentang tata letak memorinya. Standar sebenarnya hanya mengatakan bahwa jika Anda menyalin dari objek dengan tipe POD ke dalam array karakter (atau karakter unsigned) dan kembali lagi, maka hasilnya akan sama dengan objek aslinya.]

C ++ modern

Dalam versi C ++ terbaru, konsep POD dibagi antara tata letak kelas dan konstruksi, penyalinan dan penghancurannya.

Untuk kasus elipsis, ini bukan lagi perilaku yang tidak ditentukan yang sekarang didukung secara bersyarat dengan semantik yang ditentukan implementasi (N3937 - ~ C ++ '14 - 5.2.2 / 7):

... Meneruskan argumen jenis kelas yang berpotensi dievaluasi (Klausul 9) yang memiliki konstruktor salinan non-sepele, konstruktor pemindahan non-sepele, atau destruktor on-trivial, tanpa parameter yang sesuai, didukung secara kondisional dengan implementasi- semantik yang ditentukan.

Menyatakan destruktor selain =defaultberarti itu tidak sepele (12.4 / 5)

... Penghancur itu sepele jika tidak disediakan oleh pengguna ...

Perubahan lain pada C ++ Modern mengurangi dampak masalah inisialisasi agregat karena konstruktor dapat ditambahkan:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}
Richard Corden
sumber
1
Anda benar, dan saya salah, kinerja bukanlah satu-satunya alasan. Tetapi ini menunjukkan bahwa saya benar tentang sisanya: programmer kelas sebaiknya menyertakan kode untuk mencegah kelas tersebut diwarisi oleh orang lain.
Pemrogram Windows
Richard terkasih, bisakah Anda memberi komentar lebih banyak tentang apa yang telah Anda tulis. Saya tidak mengerti maksud Anda, tetapi tampaknya satu-satunya poin berharga yang saya temukan dengan googling) Atau mungkin Anda dapat memberikan tautan ke penjelasan yang lebih detail?
John Smith
1
@JohnSmith Saya telah memperbarui jawabannya. Semoga membantu.
Richard Corden
28

Saya menyatakan destruktor virtual jika dan hanya jika saya memiliki metode virtual. Setelah saya memiliki metode virtual, saya tidak percaya diri untuk menghindari membuat instance di heap atau menyimpan pointer ke kelas dasar. Keduanya adalah operasi yang sangat umum dan sering kali akan membocorkan sumber daya secara diam-diam jika destruktor tidak dinyatakan virtual.

Andy
sumber
3
Dan, pada kenyataannya, ada opsi peringatan di gcc yang memperingatkan kasus itu dengan tepat (metode virtual tetapi tidak ada dtor virtual).
CesarB
6
Tidakkah Anda kemudian mengambil risiko kebocoran memori jika Anda berasal dari kelas, terlepas dari apakah Anda memiliki fungsi virtual lainnya?
Mag Roader
1
Saya setuju dengan mag. Penggunaan destruktor virtual dan atau metode virtual adalah persyaratan terpisah. Penghancur virtual menyediakan kemampuan untuk kelas untuk melakukan pembersihan (misalnya menghapus memori, menutup file, dll ...) DAN juga memastikan konstruktor dari semua anggotanya dipanggil.
pengguna48956
7

Destruktor virtual diperlukan setiap kali ada peluang yang deletemungkin dipanggil pada penunjuk ke objek subkelas dengan tipe kelas Anda. Ini memastikan destruktor yang benar dipanggil pada waktu proses tanpa kompilator harus mengetahui kelas objek di heap pada waktu kompilasi. Misalnya, asumsikan Badalah subkelas dari A:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

Jika kode Anda tidak kritis terhadap kinerja, akan masuk akal untuk menambahkan destruktor virtual ke setiap kelas dasar yang Anda tulis, hanya untuk keamanan.

Namun, jika Anda menemukan deletebanyak objek dalam loop yang ketat, overhead kinerja pemanggilan fungsi virtual (bahkan yang kosong) mungkin terlihat. Kompiler biasanya tidak dapat menyebariskan panggilan ini, dan prosesor mungkin akan kesulitan memprediksi ke mana harus pergi. Tampaknya ini tidak akan berdampak signifikan pada kinerja, tetapi perlu disebutkan.

Jay Conrod
sumber
"Jika kode Anda tidak kritis terhadap performa, akan masuk akal untuk menambahkan destruktor virtual ke setiap kelas dasar yang Anda tulis, hanya untuk keamanan." harus lebih ditekankan dalam setiap jawaban yang saya lihat
csguy
5

Fungsi virtual berarti setiap objek yang dialokasikan meningkatkan biaya memori dengan penunjuk tabel fungsi virtual.

Jadi, jika program Anda melibatkan pengalokasian sejumlah besar objek, sebaiknya hindari semua fungsi virtual untuk menghemat tambahan 32 bit per objek.

Dalam semua kasus lain, Anda akan menyelamatkan diri Anda dari debug kesengsaraan untuk membuat dtor virtual.

mxcl
sumber
1
Hanya pilih-pilih saja, tetapi belakangan ini pointer sering kali berupa 64 bit, bukan 32.
Head Geek
5

Tidak semua kelas C ++ cocok untuk digunakan sebagai kelas dasar dengan polimorfisme dinamis.

Jika Anda ingin kelas Anda cocok untuk polimorfisme dinamis, maka destruktornya harus virtual. Selain itu, metode apa pun yang mungkin ingin ditimpa oleh subclass (yang mungkin berarti semua metode publik, ditambah kemungkinan beberapa metode terlindungi yang digunakan secara internal) harus virtual.

Jika kelas Anda tidak cocok untuk polimorfisme dinamis, maka destruktor tidak boleh ditandai virtual, karena melakukannya menyesatkan. Itu hanya mendorong orang untuk menggunakan kelas Anda secara tidak benar.

Berikut adalah contoh kelas yang tidak cocok untuk polimorfisme dinamis, meskipun destruktornya virtual:

class MutexLock {
    mutex *mtx_;
public:
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
    ~MutexLock() { mtx_->unlock(); }
private:
    MutexLock(const MutexLock &rhs);
    MutexLock &operator=(const MutexLock &rhs);
};

Inti dari kelas ini adalah untuk duduk di tumpukan untuk RAII. Jika Anda membagikan petunjuk ke objek dari kelas ini, apalagi subkelasnya, maka Anda Melakukannya Salah.

Steve Jessop
sumber
2
Penggunaan polimorfik tidak berarti penghapusan polimorfik. Ada banyak kasus penggunaan kelas yang memiliki metode virtual namun tidak ada destruktor virtual. Pertimbangkan kotak dialog yang ditentukan secara statis, di hampir semua perangkat GUI. Jendela induk akan menghancurkan objek anak, dan ia mengetahui jenis persisnya masing-masing, namun semua jendela anak juga akan digunakan secara polimorfis di sejumlah tempat, seperti pengujian hit, menggambar, API aksesibilitas yang mengambil teks untuk teks- mesin bicara, dll.
Ben Voigt
4
Benar, tetapi penanya bertanya kapan Anda harus secara khusus menghindari destruktor virtual. Untuk kotak dialog yang Anda gambarkan, penghancur virtual tidak ada gunanya, tetapi IMO tidak berbahaya. Saya tidak yakin saya akan yakin bahwa saya tidak perlu menghapus kotak dialog menggunakan penunjuk kelas dasar - misalnya saya mungkin di masa mendatang ingin jendela induk saya membuat objek anaknya menggunakan pabrik. Jadi ini bukan soal menghindari perusak virtual, hanya saja Anda mungkin tidak repot-repot memilikinya. Sebuah destructor virtual pada kelas tidak cocok untuk derivasi adalah berbahaya, meskipun, karena itu menyesatkan.
Steve Jessop
4

Alasan bagus untuk tidak mendeklarasikan destruktor sebagai virtual adalah ketika ini menyelamatkan kelas Anda dari penambahan tabel fungsi virtual, dan Anda harus menghindarinya bila memungkinkan.

Saya tahu bahwa banyak orang lebih suka untuk selalu mendeklarasikan perusak sebagai virtual, hanya untuk berada di sisi yang aman. Tetapi jika kelas Anda tidak memiliki fungsi virtual lain maka tidak ada gunanya memiliki penghancur virtual. Bahkan jika Anda memberikan kelas Anda kepada orang lain yang kemudian memperoleh kelas lain darinya maka mereka tidak akan memiliki alasan untuk memanggil delete pada pointer yang dikaburkan ke kelas Anda - dan jika mereka melakukannya maka saya akan menganggap ini sebagai bug.

Oke, ada satu pengecualian, yaitu jika kelas Anda (salah) digunakan untuk melakukan penghapusan polimorfik objek turunan, tetapi Anda - atau yang lain - semoga tahu bahwa ini memerlukan destruktor virtual.

Dengan kata lain, jika kelas Anda memiliki destruktor non-virtual maka ini adalah pernyataan yang sangat jelas: "Jangan gunakan saya untuk menghapus objek turunan!"

kidfisto
sumber
3

Jika Anda memiliki kelas yang sangat kecil dengan jumlah instance yang banyak, overhead pointer vtable dapat membuat perbedaan dalam penggunaan memori program Anda. Selama kelas Anda tidak memiliki metode virtual lainnya, menjadikan destruktor non-virtual akan menghemat overhead itu.

Mark Ransom
sumber
1

Saya biasanya mendeklarasikan virtual destruktor, tetapi jika Anda memiliki kode kritis kinerja yang digunakan dalam loop dalam, Anda mungkin ingin menghindari pencarian tabel virtual. Itu bisa menjadi penting dalam beberapa kasus, seperti pemeriksaan tabrakan. Tapi hati-hati tentang bagaimana Anda menghancurkan benda-benda itu jika Anda menggunakan warisan, atau Anda hanya akan menghancurkan setengah dari benda itu.

Perhatikan bahwa pencarian tabel virtual terjadi untuk suatu objek jika metode apa pun pada objek itu virtual. Jadi tidak ada gunanya menghapus spesifikasi virtual pada destruktor jika Anda memiliki metode virtual lain di kelas.

Jørn Jensen
sumber
1

Jika Anda benar-benar harus memastikan bahwa kelas Anda tidak memiliki vtable maka Anda juga tidak boleh memiliki destruktor virtual.

Ini adalah kasus yang jarang terjadi, tetapi memang terjadi.

Contoh paling familiar dari pola yang melakukan ini adalah kelas DirectX D3DVECTOR dan D3DMATRIX. Ini adalah metode kelas, bukan fungsi untuk gula sintaksis, tetapi kelas-kelas tersebut sengaja tidak memiliki vtabel untuk menghindari overhead fungsi karena kelas-kelas ini secara khusus digunakan dalam loop dalam dari banyak aplikasi berkinerja tinggi.

Lisa
sumber
0

Pada operasi yang akan dilakukan pada kelas dasar, dan yang seharusnya berperilaku secara virtual, harus virtual. Jika penghapusan dapat dilakukan secara polimorfis melalui antarmuka kelas dasar, maka ia harus berperilaku secara virtual dan virtual.

Destruktor tidak perlu virtual jika Anda tidak bermaksud untuk diturunkan dari kelas. Dan bahkan jika Anda melakukannya, destruktor non-virtual yang dilindungi sama baiknya jika penghapusan pointer kelas dasar tidak diperlukan .

icecrime.dll
sumber
-7

Jawaban kinerja adalah satu-satunya yang saya tahu yang memiliki peluang untuk menjadi kenyataan. Jika Anda telah mengukur dan menemukan bahwa de-virtualisasi destruktor Anda benar-benar mempercepat, maka Anda mungkin memiliki hal-hal lain di kelas itu yang perlu dipercepat juga, tetapi pada titik ini ada pertimbangan yang lebih penting. Suatu hari nanti seseorang akan menemukan bahwa kode Anda akan memberikan kelas dasar yang bagus untuk mereka dan menyimpannya untuk pekerjaan seminggu. Anda sebaiknya memastikan mereka melakukan pekerjaan minggu itu, menyalin dan menempelkan kode Anda, daripada menggunakan kode Anda sebagai basis. Sebaiknya Anda memastikan bahwa beberapa metode penting Anda bersifat pribadi sehingga tidak ada yang dapat mewarisi dari Anda.

Pemrogram Windows
sumber
Polimorfisme pasti akan memperlambat segalanya. Bandingkan dengan situasi di mana kita membutuhkan polimorfisme dan memilih untuk tidak melakukannya, itu akan menjadi lebih lambat. Contoh: kami menerapkan semua logika di destruktor kelas dasar, menggunakan RTTI dan pernyataan switch untuk membersihkan sumber daya.
sep
1
Di C ++, Anda tidak bertanggung jawab untuk menghentikan saya mewarisi dari kelas Anda yang telah Anda dokumentasikan tidak cocok untuk digunakan sebagai kelas dasar. Tanggung jawab saya adalah menggunakan warisan dengan hati-hati. Kecuali panduan gaya rumah mengatakan sebaliknya, tentu saja.
Steve Jessop
1
... hanya membuat destruktor virtual tidak berarti kelas tersebut akan bekerja dengan benar sebagai kelas dasar. Jadi menandainya sebagai virtual "hanya karena", alih-alih membuat penilaian itu, menulis cek, kode saya tidak bisa mendapatkan uang tunai.
Steve Jessop