Mengapa saya harus mendeklarasikan destruktor virtual untuk kelas abstrak di C ++?

165

Saya tahu ini adalah praktik yang baik untuk mendeklarasikan destruktor virtual untuk kelas dasar di C ++, tetapi apakah selalu penting untuk mendeklarasikan virtualdestruktor bahkan untuk kelas abstrak yang berfungsi sebagai antarmuka? Harap berikan beberapa alasan dan contoh mengapa.

Kevin
sumber

Jawaban:

196

Ini bahkan lebih penting untuk antarmuka. Setiap pengguna kelas Anda mungkin akan memegang pointer ke antarmuka, bukan pointer ke implementasi konkret. Ketika mereka datang untuk menghapusnya, jika destructor non-virtual, mereka akan memanggil destructor antarmuka (atau default yang disediakan compiler, jika Anda tidak menentukan satu), bukan destructor kelas turunan. Kebocoran memori instan.

Sebagai contoh

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}
Airsource Ltd
sumber
4
delete pmemohon perilaku yang tidak terdefinisi. Tidak dijamin untuk menelepon Interface::~Interface.
Mankarse
@Mankarse: dapatkah Anda menjelaskan apa yang menyebabkannya tidak terdefinisi? Jika Derived tidak mengimplementasikan destruktornya sendiri, apakah itu masih merupakan perilaku yang tidak terdefinisi?
Ponkadoodle
14
@Wallacoloo: Ini tidak terdefinisi karena [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Masih akan tidak terdefinisi jika Turun menggunakan destruktor yang dihasilkan secara implisit.
Mankarse
37

Jawaban atas pertanyaan Anda sering, tetapi tidak selalu. Jika kelas abstrak Anda melarang klien untuk memanggil delete pada pointer ke sana (atau jika dikatakan demikian dalam dokumentasinya), Anda bebas untuk tidak mendeklarasikan destruktor virtual.

Anda dapat melarang klien untuk memanggil delete pada sebuah pointer ke sana dengan membuat destruktornya terlindungi. Bekerja seperti ini, sangat aman dan masuk akal untuk menghilangkan destruktor virtual.

Anda pada akhirnya akan berakhir dengan tidak ada tabel metode virtual, dan akhirnya menandakan klien Anda niat Anda untuk membuatnya tidak dapat dihapus melalui pointer ke sana, jadi Anda memang punya alasan untuk tidak menyatakannya virtual dalam kasus-kasus tersebut.

[Lihat item 4 dalam artikel ini: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
sumber
Kunci untuk membuat jawaban Anda berfungsi adalah "di mana penghapusan tidak dipanggil." Biasanya jika Anda memiliki kelas dasar abstrak yang dirancang untuk menjadi antarmuka, delete akan dipanggil pada kelas antarmuka.
John Dibling
Seperti yang ditunjukkan John di atas, apa yang Anda sarankan sangat berbahaya. Anda mengandalkan asumsi bahwa klien dari antarmuka Anda tidak akan pernah menghancurkan objek yang hanya mengetahui tipe dasar. Satu-satunya cara Anda dapat menjamin bahwa jika itu nonvirtual adalah membuat kelas abstrak dilindungi.
Michel
Michel, saya telah mengatakan demikian :) "Jika Anda melakukannya, Anda membuat destructor Anda terlindungi. Jika Anda melakukannya, klien tidak akan dapat menghapus menggunakan pointer ke antarmuka itu." dan memang itu tidak bergantung pada klien, tetapi harus memaksanya memberi tahu klien "Anda tidak bisa melakukan ...". Saya tidak melihat bahaya apa pun
Johannes Schaub - litb
saya memperbaiki kata-kata buruk dari jawaban saya sekarang. itu menyatakan secara eksplisit sekarang bahwa itu tidak bergantung pada klien. sebenarnya saya pikir itu jelas bahwa mengandalkan klien melakukan sesuatu adalah keluar dari jalan. terima kasih :)
Johannes Schaub - litb
2
+1 untuk menyebutkan destruktor yang dilindungi, yang merupakan "jalan keluar" lain dari masalah tidak sengaja memanggil destruktor yang salah ketika menghapus pointer ke kelas dasar.
j_random_hacker
23

Saya memutuskan untuk melakukan riset dan mencoba merangkum jawaban Anda. Pertanyaan-pertanyaan berikut akan membantu Anda memutuskan jenis destruktor apa yang Anda butuhkan:

  1. Apakah kelas Anda dimaksudkan untuk digunakan sebagai kelas dasar?
    • Tidak: Nyatakan destruktor publik non-virtual untuk menghindari v-pointer pada setiap objek kelas * .
    • Ya: Baca pertanyaan berikutnya.
  2. Apakah kelas dasar Anda abstrak? (Yaitu metode virtual murni?)
    • Tidak: Cobalah untuk membuat abstrak kelas dasar Anda dengan mendesain ulang hierarki kelas Anda
    • Ya: Baca pertanyaan berikutnya.
  3. Apakah Anda ingin mengizinkan penghapusan polimorfik melalui basis pointer?
    • Tidak: Nyatakan destruktor virtual yang dilindungi untuk mencegah penggunaan yang tidak diinginkan.
    • Ya: Nyatakan destruktor virtual publik (tidak ada overhead dalam kasus ini).

Saya harap ini membantu.

* Penting untuk dicatat bahwa tidak ada cara di C ++ untuk menandai kelas sebagai final (yaitu non subclassable), jadi jika Anda memutuskan untuk mendeklarasikan destruktor Anda non-virtual dan publik, ingatlah untuk secara eksplisit memperingatkan sesama programmer terhadap berasal dari kelas Anda.

Referensi:

davidag
sumber
11
Jawaban ini sebagian sudah usang, sekarang ada kata kunci terakhir dalam C ++.
Étienne
10

Ya itu selalu penting. Kelas turunan dapat mengalokasikan memori atau menyimpan referensi ke sumber daya lain yang perlu dibersihkan ketika objek dihancurkan. Jika Anda tidak memberikan antarmuka / kelas abstrak destruktor virtual Anda, maka setiap kali Anda menghapus turunan kelas turunan melalui kelas dasar menangani destruktor kelas turunan Anda tidak akan dipanggil.

Karenanya, Anda membuka potensi kebocoran memori

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
OJ.
sumber
Benar, pada kenyataannya dalam contoh itu, itu mungkin bukan hanya kehabisan memori kebocoran, tetapi mungkin crash: - /
Evan Teran
7

Itu tidak selalu diperlukan, tetapi saya menemukan itu sebagai praktik yang baik. Apa yang dilakukannya, adalah memungkinkan objek yang diturunkan dihapus dengan aman melalui pointer dari tipe dasar.

Jadi misalnya:

Base *p = new Derived;
// use p as you see fit
delete p;

terbentuk dengan buruk jika Basetidak memiliki destruktor virtual, karena ia akan berusaha untuk menghapus objek seolah-olah itu adalah Base *.

Evan Teran
sumber
Anda tidak ingin memperbaiki boost :: shared_pointer p (Berasal baru) agar terlihat seperti boost :: shared_pointer <Base> p (Berasal baru); ? mungkin ppl akan mengerti jawaban Anda saat itu dan memilih
Johannes Schaub - litb
Suntingan: "Dikodifikasi" beberapa bagian untuk membuat kurung sudut terlihat, seperti yang disarankan litb.
j_random_hacker
@EvanTeran: Saya tidak yakin apakah ini telah berubah sejak jawaban awalnya diposting (dokumentasi Boost di boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm menyarankannya), tetapi itu tidak benar hari ini yang shared_ptrakan mencoba untuk menghapus objek seolah-olah itu Base *- ia mengingat jenis benda yang Anda buat dengannya. Lihat tautan yang direferensikan, khususnya bit yang mengatakan "Destructor akan memanggil delete dengan pointer yang sama, lengkap dengan tipe aslinya, bahkan ketika T tidak memiliki destruktor virtual, atau batal."
Stuart Golodetz
@ StuartGolodetz: Hmm, Anda mungkin benar, tapi saya jujur ​​tidak yakin. Mungkin masih terbentuk buruk dalam konteks ini karena kurangnya destruktor virtual. Ini layak untuk dilihat.
Evan Teran
@EvanTeran: Seandainya bermanfaat - stackoverflow.com/questions/3899790/share-ptr-magic .
Stuart Golodetz
5

Ini bukan hanya latihan yang bagus. Ini adalah aturan # 1 untuk hierarki kelas apa pun.

  1. Basis sebagian besar kelas hierarki di C ++ harus memiliki destruktor virtual

Sekarang untuk Mengapa. Ambil hierarki hewan yang khas. Destructor virtual melalui pengiriman virtual seperti halnya metode lain memanggil. Ambil contoh berikut.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Asumsikan bahwa Hewan adalah kelas abstrak. Satu-satunya cara C ++ mengetahui destructor yang tepat untuk dipanggil adalah melalui metode pengiriman virtual. Jika destruktor itu bukan virtual maka ia hanya akan memanggil destruktor Animal dan tidak menghancurkan objek apa pun di kelas turunan.

Alasan untuk membuat virtual destruktor di kelas dasar adalah karena ia hanya menghilangkan pilihan dari kelas turunan. Destructor mereka menjadi virtual secara default.

JaredPar
sumber
2
Saya sebagian besar setuju dengan Anda, karena biasanya ketika mendefinisikan hierarki Anda ingin dapat merujuk ke objek turunan menggunakan pointer kelas dasar / referensi. Tapi itu tidak selalu terjadi, dan dalam kasus-kasus lain, mungkin cukup untuk membuat kelas dasar sebagai gantinya dilindungi.
j_random_hacker
@j_random_hacker membuatnya terlindungi tidak akan melindungi Anda dari penghapusan internal yang tidak benar
JaredPar
1
@JaredPar: Itu benar, tetapi setidaknya Anda dapat bertanggung jawab dalam kode Anda sendiri - yang sulit adalah memastikan bahwa kode klien tidak dapat menyebabkan kode Anda meledak. (Demikian pula, membuat data anggota pribadi tidak mencegah kode internal melakukan sesuatu yang bodoh dengan anggota itu.)
j_random_hacker
@j_random_hacker, maaf untuk membalas dengan posting blog tetapi sangat cocok dengan skenario ini. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar
@JaredPar: Pos luar biasa, saya setuju dengan Anda 100%, terutama tentang memeriksa kontrak dalam kode ritel. Maksud saya ada beberapa kasus ketika Anda tahu Anda tidak perlu virtual dtor. Contoh: kelas tag untuk pengiriman template. Mereka memiliki ukuran 0, Anda hanya menggunakan warisan untuk menunjukkan spesialisasi.
j_random_hacker
3

Jawabannya sederhana, Anda harus virtual jika tidak, kelas dasar tidak akan menjadi kelas polimorfik lengkap.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Anda lebih suka penghapusan di atas, tetapi jika destructor kelas dasar bukan virtual, hanya destruktor kelas dasar akan dipanggil dan semua data dalam kelas turunan akan tetap terhapus.

fatma.ekici
sumber