Adakah alasan bagus untuk tidak mendeklarasikan destruktor virtual untuk kelas? Kapan sebaiknya Anda secara khusus menghindari menulisnya?
Adakah alasan bagus untuk tidak mendeklarasikan destruktor virtual untuk kelas? Kapan sebaiknya Anda secara khusus menghindari menulisnya?
Jawaban:
Tidak perlu menggunakan destruktor virtual jika salah satu hal di bawah ini benar:
Tidak ada alasan khusus untuk menghindarinya kecuali jika Anda benar-benar terdesak untuk mengingat.
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 =default
berarti 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
}
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.
Destruktor virtual diperlukan setiap kali ada peluang yang delete
mungkin 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 B
adalah 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 delete
banyak 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.
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.
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.
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!"
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.
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.
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.
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 .
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.