Mengapa kita membutuhkan destruktor virtual murni di C ++?

154

Saya mengerti perlunya destruktor virtual. Tetapi mengapa kita membutuhkan destruktor virtual murni? Dalam salah satu artikel C ++, penulis telah menyebutkan bahwa kami menggunakan destructor virtual murni ketika kami ingin membuat abstrak kelas.

Tetapi kita dapat membuat abstrak kelas dengan membuat salah satu fungsi anggota sebagai virtual murni.

Jadi pertanyaan saya adalah

  1. Kapan kita benar-benar membuat virtual destructor murni? Adakah yang bisa memberikan contoh waktu nyata yang baik?

  2. Ketika kita membuat kelas abstrak, apakah itu praktik yang baik untuk membuat destructor juga virtual murni? Jika ya..kemudian mengapa?

Menandai
sumber
14
@ Daniel- Tautan yang disebutkan tidak menjawab pertanyaan saya. Ini menjawab mengapa destruktor virtual murni harus memiliki definisi. Pertanyaan saya adalah mengapa kita membutuhkan destructor virtual murni.
Markus
Saya mencoba mencari tahu alasannya, tetapi Anda sudah mengajukan pertanyaan di sini.
nsivakr

Jawaban:

119
  1. Mungkin alasan sebenarnya bahwa penghancur virtual murni diizinkan adalah bahwa untuk melarang mereka berarti menambahkan aturan lain ke bahasa dan tidak perlu untuk aturan ini karena tidak ada efek buruk dapat datang dari memungkinkan destruktor virtual murni.

  2. Tidak, virtual tua biasa sudah cukup.

Jika Anda membuat objek dengan implementasi default untuk metode virtualnya dan ingin membuatnya abstrak tanpa memaksa siapa pun untuk menimpa metode spesifik apa pun , Anda bisa membuat destructor virtual murni. Saya tidak melihat banyak gunanya tapi itu mungkin.

Perhatikan bahwa karena kompiler akan menghasilkan destruktor implisit untuk kelas turunan, jika penulis kelas tidak melakukannya, kelas turunan apa pun tidak akan abstrak. Oleh karena itu memiliki destruktor virtual murni di kelas dasar tidak akan membuat perbedaan untuk kelas turunan. Itu hanya akan membuat abstrak kelas dasar (terima kasih atas komentar @kappa ).

Orang mungkin juga berasumsi bahwa setiap kelas turunan mungkin perlu memiliki kode pembersihan khusus dan menggunakan destruktor virtual murni sebagai pengingat untuk menulis satu tetapi ini tampaknya dibuat-buat (dan tidak diperkuat).

Catatan: destructor adalah satu-satunya metode yang bahkan jika itu adalah murni maya memiliki untuk memiliki implementasi untuk instantiate kelas turunan (fungsi virtual ya murni dapat memiliki implementasi).

struct foo {
    virtual void bar() = 0;
};

void foo::bar() { /* default implementation */ }

class foof : public foo {
    void bar() { foo::bar(); } // have to explicitly call default implementation.
};
Motti
sumber
13
"Ya fungsi virtual murni dapat memiliki implementasi" Maka itu bukan virtual murni.
GManNickG
2
Jika Anda ingin membuat abstrak kelas, bukankah lebih mudah untuk membuat semua konstruktor terlindungi?
bdonlan
78
@ GM, Anda salah, menjadi virtual murni berarti kelas turunan harus mengganti metode ini, ini orthogonal untuk memiliki implementasi. Periksa kode saya dan komentar foof::barjika Anda ingin melihatnya sendiri.
Motti
15
@ GM: lite C ++ FAQ mengatakan "Perhatikan bahwa dimungkinkan untuk memberikan definisi untuk fungsi virtual murni, tetapi ini biasanya membingungkan pemula dan sebaiknya dihindari sampai nanti." parashift.com/c++-faq-lite/abcs.html#faq-22.4 Wikipedia (benteng kebenaran) juga mengatakan hal yang sama. Saya percaya standar ISO / IEC menggunakan terminologi yang sama (sayangnya salinan saya sedang bekerja saat ini) ... Saya setuju bahwa itu membingungkan, dan saya biasanya tidak menggunakan istilah tanpa klarifikasi ketika saya memberikan definisi, terutama sekitar programmer baru ...
leander
9
@Otti: Apa yang menarik di sini dan memberikan lebih banyak kebingungan adalah bahwa destruktor virtual murni TIDAK perlu secara eksplisit ditimpa dalam kelas turunan (dan instantiated). Dalam kasus seperti itu definisi implisit digunakan :)
kappa
33

Yang Anda butuhkan untuk kelas abstrak adalah setidaknya satu fungsi virtual murni. Fungsi apa pun akan dilakukan; tetapi ketika itu terjadi, destructor adalah sesuatu yang dimiliki oleh setiap kelas — jadi selalu ada sebagai kandidat. Lebih jauh lagi, membuat destructor virtual murni (bukan hanya virtual) tidak memiliki efek samping perilaku selain membuat kelas abstrak. Dengan demikian, banyak panduan gaya merekomendasikan bahwa destuctor virtual murni digunakan secara konsisten untuk menunjukkan bahwa suatu kelas adalah abstrak — jika tanpa alasan lain selain menyediakan tempat yang konsisten, seseorang yang membaca kode dapat melihat apakah kelas itu abstrak.

Braden
sumber
1
tetapi masih mengapa untuk menyediakan implementasi destructor virtaul murni. Apa yang mungkin salah, saya membuat virtual destructor murni dan tidak menyediakan implementasinya. Saya berasumsi hanya pointer kelas dasar yang dinyatakan dan karenanya destruktor untuk kelas abstrak tidak pernah dipanggil.
Krishna Oza
4
@ Surfing: karena destruktor dari kelas turunan secara implisit memanggil destruktor dari kelas dasarnya, bahkan jika destruktor itu murni virtual. Jadi jika tidak ada implementasi untuk itu bahavior yang tidak ditentukan akan terjadi.
a.peganz
19

Jika Anda ingin membuat kelas dasar abstrak:

  • itu tidak bisa dipakai (ya, ini berlebihan dengan istilah "abstrak"!)
  • tetapi membutuhkan perilaku destruktor virtual (Anda bermaksud membawa pointer ke ABC daripada pointer ke tipe turunan, dan menghapusnya)
  • tetapi tidak memerlukan pengiriman virtual lain perilaku untuk metode lainnya (mungkin ada yang tidak ada metode lain? mempertimbangkan sederhana dilindungi "sumber daya" wadah yang membutuhkan konstruktor / destruktor / tugas tapi tidak banyak lagi)

... paling mudah untuk membuat abstrak kelas dengan membuat destructor dan virtual murni memberikan definisi (metode body) untuk itu.

Untuk ABC hipotetis kami:

Anda menjamin bahwa itu tidak dapat dipakai (bahkan internal ke kelas itu sendiri, ini sebabnya konstruktor pribadi mungkin tidak cukup), Anda mendapatkan perilaku virtual yang Anda inginkan untuk destruktor, dan Anda tidak perlu menemukan dan menandai metode lain yang tidak perlu pengiriman virtual sebagai "virtual".

Leander
sumber
8

Dari jawaban yang saya baca untuk pertanyaan Anda, saya tidak dapat menyimpulkan alasan yang bagus untuk benar-benar menggunakan destructor virtual murni. Misalnya, alasan berikut tidak meyakinkan saya sama sekali:

Mungkin alasan sebenarnya bahwa penghancur virtual murni diizinkan adalah bahwa untuk melarang mereka berarti menambahkan aturan lain ke bahasa dan tidak perlu untuk aturan ini karena tidak ada efek buruk dapat datang dari memungkinkan destruktor virtual murni.

Menurut pendapat saya, destruktor virtual murni dapat bermanfaat. Misalnya, anggap Anda memiliki dua kelas myClassA dan myClassB dalam kode Anda, dan bahwa myClassB mewarisi dari myClassA. Untuk alasan yang disebutkan oleh Scott Meyers dalam bukunya "Lebih Efektif C ++", Butir 33 "Membuat abstrak kelas non-daun", adalah praktik yang lebih baik untuk benar-benar membuat kelas abstrak myAbstractClass dari mana myClassA dan myClassB mewarisi. Ini memberikan abstraksi yang lebih baik dan mencegah beberapa masalah yang timbul dengan, misalnya, salinan objek.

Dalam proses abstraksi (membuat kelas myAbstractClass), bisa jadi tidak ada metode myClassA atau myClassB adalah kandidat yang baik untuk menjadi metode virtual murni (yang merupakan prasyarat untuk myAbstractClass menjadi abstrak). Dalam hal ini, Anda mendefinisikan destructor virtual murni kelas abstrak.

Selanjutnya contoh nyata dari beberapa kode yang saya tulis sendiri. Saya memiliki dua kelas, Numerik / PhysicsParams yang berbagi properti umum. Karena itu saya membiarkan mereka mewarisi dari kelas abstrak IParams. Dalam hal ini, saya sama sekali tidak punya metode yang bisa murni virtual. Metode setParameter, misalnya, harus memiliki tubuh yang sama untuk setiap subkelas. Satu-satunya pilihan yang saya miliki adalah membuat destructor IParams menjadi virtual.

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};
Laurent Michel
sumber
1
Saya suka penggunaan ini, tetapi cara lain untuk "menegakkan" warisan adalah dengan mendeklarasikan konstruktor IParamuntuk dilindungi, seperti yang dicatat dalam beberapa komentar lainnya.
rwols
4

Jika Anda ingin berhenti membuat instance dari kelas dasar tanpa membuat perubahan dalam kelas turunan Anda yang sudah diimplementasikan dan diuji, Anda menerapkan destruktor virtual murni di kelas dasar Anda.

sukumar
sumber
3

Di sini saya ingin memberi tahu kapan kita membutuhkan destruktor virtual dan kapan kita membutuhkan destruktor virtual murni

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. Bila Anda ingin tidak ada yang bisa membuat objek kelas Base secara langsung, gunakan destruktor virtual murni virtual ~Base() = 0. Biasanya setidaknya diperlukan satu fungsi virtual murni, mari kita ambil virtual ~Base() = 0, karena fungsi ini.

  2. Ketika Anda tidak perlu hal di atas, hanya Anda yang membutuhkan penghancuran yang aman dari objek kelas turunan

    Base * pBase = new Derived (); hapus pBase; destructor virtual murni tidak diperlukan, hanya destruktor virtual yang akan melakukan pekerjaan.

Anil8753
sumber
2

Anda masuk ke hipotesis dengan jawaban ini, jadi saya akan mencoba untuk membuat penjelasan yang lebih sederhana, lebih membumi untuk kejelasan demi.

Hubungan dasar desain berorientasi objek adalah dua: IS-A dan HAS-A. Saya tidak mengada-ada. Itulah sebutan mereka.

IS-A menunjukkan bahwa objek tertentu mengidentifikasi sebagai kelas yang di atasnya dalam hirarki kelas. Objek pisang adalah objek buah jika merupakan subkelas dari kelas buah. Ini berarti bahwa di mana pun kelas buah dapat digunakan, pisang dapat digunakan. Ini bukan refleksif. Anda tidak dapat mengganti kelas dasar untuk kelas tertentu jika kelas spesifik itu dipanggil.

Has-a menunjukkan bahwa objek adalah bagian dari kelas komposit dan ada hubungan kepemilikan. Ini berarti dalam C ++ bahwa itu adalah objek anggota dan dengan demikian tanggung jawab berada pada kelas pemilik untuk membuangnya atau melepaskan kepemilikan sebelum merusak dirinya sendiri.

Kedua konsep ini lebih mudah diwujudkan dalam bahasa pewarisan tunggal daripada dalam model pewarisan berganda seperti c ++, tetapi aturan dasarnya sama. Komplikasi muncul ketika identitas kelas ambigu, seperti meneruskan pointer kelas Banana ke fungsi yang mengambil pointer kelas Fruit.

Fungsi virtual adalah, pertama, hal run-time. Ini adalah bagian dari polimorfisme karena digunakan untuk memutuskan fungsi mana yang akan dijalankan pada saat dipanggil dalam program yang sedang berjalan.

Kata kunci virtual adalah arahan kompiler untuk mengikat fungsi dalam urutan tertentu jika ada ambiguitas tentang identitas kelas. Fungsi virtual selalu dalam kelas induk (sejauh yang saya tahu) dan menunjukkan kepada kompiler bahwa pengikatan fungsi anggota dengan nama mereka harus dilakukan dengan fungsi subkelas terlebih dahulu dan fungsi kelas induk setelahnya.

Kelas Buah dapat memiliki warna fungsi virtual () yang mengembalikan "NONE" secara default. Fungsi warna kelas Pisang () mengembalikan "YELLOW" atau "BROWN".

Tetapi jika fungsi yang mengambil pointer Buah memanggil warna () pada kelas Pisang yang dikirim kepadanya - fungsi warna () mana yang dipanggil? Fungsi biasanya akan memanggil Buah :: warna () untuk objek Buah.

Itu akan 99% dari waktu tidak menjadi apa yang dimaksudkan. Tetapi jika Fruit :: color () dinyatakan virtual maka Banana: color () akan dipanggil untuk objek karena fungsi color () yang benar akan terikat ke pointer Fruit pada saat panggilan. Runtime akan memeriksa objek apa yang ditunjuk oleh pointer karena itu ditandai virtual dalam definisi kelas Buah.

Ini berbeda dari mengesampingkan fungsi dalam subkelas. Dalam hal itu, pointer Buah akan memanggil Buah :: warna () jika yang diketahuinya adalah pointer IS-A ke Buah.

Jadi sekarang muncul ide "fungsi virtual murni". Ini adalah ungkapan yang agak disayangkan karena kemurnian tidak ada hubungannya dengan itu. Ini berarti bahwa ini dimaksudkan agar metode kelas dasar tidak pernah dipanggil. Memang fungsi virtual murni tidak bisa disebut. Namun itu masih harus didefinisikan. Tanda tangan fungsi harus ada. Banyak coder membuat implementasi kosong {} untuk kelengkapan, tetapi kompiler akan menghasilkan satu secara internal jika tidak. Dalam hal ini ketika fungsi dipanggil bahkan jika pointer ke Buah, Banana :: color () akan dipanggil karena hanya implementasi warna () yang ada.

Sekarang bagian terakhir dari teka-teki: konstruktor dan destruktor.

Konstruktor virtual murni ilegal, sepenuhnya. Itu baru saja keluar.

Tetapi destruktor virtual murni berfungsi jika Anda ingin melarang pembuatan instance kelas dasar. Hanya sub-kelas yang dapat dipakai jika destruktor dari kelas dasar adalah virtual murni. konvensi adalah untuk menetapkannya ke 0.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

Anda harus membuat implementasi dalam hal ini. Kompiler tahu ini adalah apa yang Anda lakukan dan memastikan Anda melakukannya dengan benar, atau mengeluh bahwa ia tidak dapat menautkan ke semua fungsi yang diperlukan untuk dikompilasi. Kesalahan bisa membingungkan jika Anda tidak berada di jalur yang benar tentang bagaimana Anda memodelkan hierarki kelas Anda.

Jadi Anda dilarang dalam hal ini untuk membuat contoh Buah, tetapi diizinkan untuk membuat contoh Pisang.

Panggilan untuk menghapus pointer Buah yang menunjuk ke instance Banana akan memanggil Banana :: ~ Banana () terlebih dahulu dan kemudian memanggil Fuit :: ~ Fruit (), selalu. Karena bagaimanapun caranya, ketika Anda memanggil destruktor subclass, destructor kelas dasar harus mengikuti.

Apakah ini model yang buruk? Ini lebih rumit pada tahap desain, ya, tetapi dapat memastikan bahwa penghubungan yang benar dilakukan pada saat run-time dan bahwa fungsi subclass dilakukan di mana ada ambiguitas untuk tepatnya subclass mana yang sedang diakses.

Jika Anda menulis C ++ sehingga Anda hanya membagikan pointer kelas yang tepat tanpa pointer umum atau ambigu, maka fungsi virtual tidak benar-benar diperlukan. Tetapi jika Anda memerlukan fleksibilitas jenis waktu berjalan (seperti pada Apple Banana Orange ==> Buah) fungsi menjadi lebih mudah dan lebih fleksibel dengan kode yang tidak terlalu banyak. Anda tidak lagi harus menulis fungsi untuk setiap jenis buah, dan Anda tahu bahwa setiap buah akan merespons warna () dengan fungsi yang benar.

Saya harap penjelasan yang bertele-tele ini memperkuat konsep daripada membingungkan hal-hal. Ada banyak contoh bagus di luar sana untuk dilihat, dan melihat cukup dan benar-benar menjalankannya dan mengacaukannya dan Anda akan mendapatkannya.

Chris Reid
sumber
1

Ini adalah topik lama yang sudah ada satu dekade :) Bacalah 5 paragraf terakhir dari Item # 7 pada buku "Effective C ++" untuk detailnya, dimulai dari "Kadang-kadang lebih mudah untuk memberi kelas destruktor virtual murni ...."

JQ
sumber
0

Anda meminta contoh, dan saya percaya yang berikut ini memberikan alasan untuk destruktor virtual murni. Saya menantikan balasan, apakah ini alasan yang bagus ...

Saya tidak ingin ada yang bisa melempar error_basetipe, tetapi tipe pengecualian error_oh_shucksdan error_oh_blastmemiliki fungsi yang sama dan saya tidak ingin menulisnya dua kali. Kompleksitas pImpl diperlukan untuk menghindari pemaparan std::stringkepada klien saya, dan penggunaan std::auto_ptrmengharuskan pembuat salinan.

Header publik berisi spesifikasi pengecualian yang akan tersedia untuk klien untuk membedakan berbagai jenis pengecualian yang dilemparkan oleh perpustakaan saya:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

Dan di sini adalah implementasi bersama:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

Kelas exception_string, dirahasiakan, menyembunyikan std :: string dari antarmuka publik saya:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

Kode saya kemudian menimbulkan kesalahan sebagai:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

Penggunaan templat untuk errorsedikit serampangan. Ini menghemat sedikit kode dengan mengorbankan mengharuskan klien untuk menangkap kesalahan sebagai:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}
Rai
sumber
0

Mungkin ada lagi PENGGUNAAN KASUS NYATA dari destruktor virtual murni yang sebenarnya tidak bisa saya lihat di jawaban lain :)

Pada awalnya, saya sepenuhnya setuju dengan jawaban yang ditandai: Itu karena melarang destructor virtual murni akan memerlukan aturan tambahan dalam spesifikasi bahasa. Tapi itu masih bukan use case yang Mark minta :)

Pertama bayangkan ini:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

dan sesuatu seperti:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

Cukup - kami memiliki antarmuka Printabledan beberapa "wadah" memegang apa pun dengan antarmuka ini. Saya pikir di sini cukup jelas mengapa print()metode virtual murni. Itu bisa memiliki beberapa badan tetapi jika tidak ada implementasi standar, virtual murni adalah "implementasi" yang ideal (= "harus disediakan oleh kelas turunan").

Dan sekarang bayangkan persis sama kecuali bukan untuk mencetak tetapi untuk kehancuran:

class Destroyable {
  virtual ~Destroyable() = 0;
};

Dan juga mungkin ada wadah serupa:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

Ini kasus penggunaan yang disederhanakan dari aplikasi saya yang sebenarnya. Satu-satunya perbedaan di sini adalah bahwa metode "khusus" (destruktor) digunakan daripada "normal"print() . Tetapi alasan mengapa itu murni virtual masih sama - tidak ada kode default untuk metode ini. Agak membingungkan bisa menjadi kenyataan bahwa HARUS ada beberapa destruktor secara efektif dan kompiler benar-benar menghasilkan kode kosong untuk itu. Tetapi dari perspektif seorang programmer, virtualitas murni masih berarti: "Saya tidak punya kode default, itu harus disediakan oleh kelas turunan."

Saya pikir ini bukan ide besar di sini, hanya lebih banyak penjelasan bahwa virtualitas murni bekerja sangat seragam - juga untuk para penghancur.

Jarek C
sumber
-2

1) Ketika Anda ingin meminta kelas turunan untuk melakukan pembersihan. Ini jarang terjadi.

2) Tidak, tetapi Anda ingin itu virtual, meskipun.

Steven Sudit
sumber
-2

kita perlu membuat destructor virtual karena fakta bahwa, jika kita tidak membuat virtual destructor maka kompiler hanya akan merusak isi kelas dasar, n semua kelas turunan akan tetap tidak berubah, kompiler bacuse tidak akan memanggil destruktor yang lain kelas kecuali kelas dasar.

Asad hashmi
sumber
-1: Pertanyaannya bukan tentang mengapa destructor harus virtual.
Troubadour
Selain itu, dalam situasi tertentu penghancur tidak harus virtual untuk mencapai kehancuran yang benar. Destructor virtual hanya diperlukan ketika Anda akhirnya memanggil deletepointer ke kelas dasar padahal sebenarnya menunjuk ke turunannya.
CygnusX1
Anda 100% benar. Ini adalah dan telah di masa lalu salah satu sumber nomor satu kebocoran dan crash dalam program C ++, ketiga hanya untuk mencoba melakukan hal-hal dengan pointer nol dan melebihi batas array. Sebuah destruktor kelas dasar non-virtual akan dipanggil pada pointer generik, melewati destruktor subclass sepenuhnya jika tidak ditandai virtual. Jika ada objek yang dibuat secara dinamis milik subclass, mereka tidak akan dipulihkan oleh destruktor dasar pada panggilan untuk menghapus. Anda menenggak baik maka BLUURRK! (Sulit untuk menemukan di mana juga.)
Chris Reid