C ++で抽象クラスの仮想デストラクタを宣言する必要があるのはなぜですか?


165

C ++で基本クラスの仮想デストラクタを宣言することは良い習慣であることはわかってvirtualいますが、インターフェイスとして機能する抽象クラスでも、デストラクタを宣言することは常に重要ですか?いくつかの理由と理由を教えてください。

回答:


196

それはインターフェースにとってさらに重要です。クラスのユーザーはおそらく、具体的な実装へのポインターではなく、インターフェイスへのポインターを保持します。デストラクタが非仮想である場合、それらが削除されると、派生クラスのデストラクタではなく、インターフェイスのデストラクタ(または、コンパイラが提供するデフォルトを指定しなかった場合)を呼び出します。インスタントメモリリーク。

例えば

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; 
}

4
delete p未定義の動作を呼び出します。の呼び出しは保証されていませんInterface::~Interface
マンカルス

@マンカルセ:それが未定義になる原因を説明できますか?Derivedが独自のデストラクタを実装しなかった場合でも、それは未定義の動作ですか?
Ponkadoodle 2012年

14
@Wallacoloo:次の理由により未定義です[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. ...。Derivedが暗黙的に生成されたデストラクタを使用した場合でも、未定義になります。
Mankarse 2012年

37

あなたの質問への答えはしばしばですが、いつもではありません。抽象クラスがクライアントへのポインタでdeleteを呼び出すことを禁止している場合(またはドキュメントにそのように記載されている場合)、仮想デストラクタを宣言しないことは自由です。

デストラクタを保護することで、クライアントがポインタを削除するのを禁止できます。このように機能するため、仮想デストラクタを省略することは完全に安全で合理的​​です。

最終的には仮想メソッドテーブルがなくなり、ポインタを介してそれを削除不可にする意図をクライアントに通知することになるので、これらのケースでは実際に仮想メソッドを宣言しない理由があります。

[この記事の項目4を参照してください:http : //www.gotw.ca/publications/mill18.htm ]


回答を機能させるための鍵は、「削除が要求されない」ことです。通常、インターフェイスとして設計された抽象基本クラスがある場合、削除はインターフェイスクラスで呼び出されます。
John Dibling 2008年

上記のジョンが指摘したように、あなたの提案はかなり危険です。あなたはあなたのインターフェースのクライアントが基本型だけを知っているオブジェクトを決して破壊しないという仮定に依存しています。非仮想の場合は、抽象クラスのdtorを保護することを保証できる唯一の方法です。
ミシェル

ミシェル、私はそう言った:)「そうするなら、あなたはデストラクタを保護する。そうするなら、クライアントはそのインターフェースへのポインタを使って削除することができないだろう。」実際、クライアントに依存しているわけではありませんが、強制的にクライアントに「実行できない...」と伝える必要があります。何の危険も見えない
ヨハネスシャウブ-litb '28

私は今私の答えの悪い言葉遣いを修正しました。それはクライアントに依存しないことを明示的に述べています。とにかく、クライアントが何かを行うことに依存することはとにかく邪魔にならないことは明らかだと思いました。おかげで:)
ヨハネス・シャウブ-litb 2008年

2
保護されたデストラクタに言及するための+1。これは、基本クラスへのポインタを削除するときに誤ってデストラクタを誤って呼び出す問題のもう1つの「回避策」です。
j_random_hacker 2009

23

私はいくつかの調査を行い、あなたの答えを要約しようとすることにしました。次の質問は、必要なデストラクタの種類を決定するのに役立ちます。

  1. あなたのクラスは基本クラスとして使用することを意図していますか?
    • いいえ:クラス*の各オブジェクトのvポインターを回避するために、パブリック非仮想デストラクタを宣言します。
    • はい:次の質問を読んでください。
  2. 基本クラスは抽象ですか?(つまり、仮想的な純粋なメソッドはありますか?)
    • いいえ:クラス階層を再設計して、基本クラスを抽象化するようにしてください
    • はい:次の質問を読んでください。
  3. ベースポインターによる多態性の削除を許可しますか?
    • いいえ:保護された仮想デストラクタを宣言して、不要な使用を防止します。
    • はい:パブリック仮想デストラクタを宣言します(この場合、オーバーヘッドはありません)。

これがお役に立てば幸いです。

* C ++ではクラスを最終(つまりサブクラス化不可)としてマークする方法がないことに注意することが重要です。そのため、デストラクタを非仮想かつパブリックとして宣言する場合は、他のプログラマに対して明示的に警告することを忘れないでください。クラスから派生します。

参照:


11
この回答は部分的に古くなっており、C ++には最後のキーワードがあります。
エティエンヌ

10

はい、それは常に重要です。派生クラスは、メモリを割り当てたり、オブジェクトが破棄されたときにクリーンアップする必要がある他のリソースへの参照を保持したりできます。インターフェース/抽象クラスに仮想デストラクタを指定しない場合、基本クラスハンドルを介して派生クラスインスタンスを削除するたびに、派生クラスのデストラクタは呼び出されません。

したがって、メモリリークの可能性を切り開いています

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

確かに、実際にはその例では、メモリリークだけでなく、クラッシュする可能性もあります:-/
Evan Teran

7

常に必要なわけではありませが、良い方法だと思います。それが何をするか、それは派生型が基本型のポインタを通して安全に削除されることを可能にすることです

だから例えば:

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

Base仮想デストラクタを持たない場合は、形式が正しくありませんBase *。オブジェクトがであるかのようにオブジェクトを削除しようとするためです。


boost :: shared_pointer p(new Derived)をboost :: shared_pointer <Base> p(new Derived);のように修正したくないですか。?多分皆さんは、あなたの答えを理解し、投票します
ヨハネス・シャウブを- litb

編集:litbが提案するように、山かっこを表示するためにいくつかのパーツを「コード化」しました。
j_random_hacker 2009

@EvanTeran:回答が最初に投稿されてから変更されているかどうかはわかりません(boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htmにあるBoostのドキュメントにある可能性があることが示唆されています)。最近のshared_ptrように、オブジェクトを削除しようBase *とします。作成したもののタイプを記憶しています。参照されているリンク、特に「デストラクタは、Tに仮想デストラクタがないか、またはvoidである場合でも、元の型で完全な同じポインタで削除を呼び出す」と書かれているビットを参照してください。
スチュアートゴロデッツ

@StuartGolodetz:うーん、あなたは正しいかもしれませんが、私は正直にわかりません。仮想デストラクタが不足しているため、このコンテキストではまだ正しく形成されていない可能性があります。じっくりと検討する価値があります。
Evan Teran

@EvanTeran:ケースで、それは便利です- stackoverflow.com/questions/3899790/shared-ptr-magic
スチュアートゴロデッツ

5

それは良い習慣だけではありません。これは、クラス階層のルール#1です。

  1. C ++の階層の最も基本的なクラスには、仮想デストラクタが必要です

なぜか。典型的な動物の階層を取り上げます。仮想デストラクタは、他のメソッド呼び出しと同じように仮想ディスパッチを実行します。次の例を見てください。

Animal* pAnimal = GetAnimal();
delete pAnimal;

Animalは抽象クラスであると仮定します。C ++が呼び出す適切なデストラクタを認識する唯一の方法は、仮想メソッドディスパッチを介することです。デストラクタが仮想でない場合は、単に動物のデストラクタを呼び出し、派生クラスのオブジェクトは破棄しません。

基本クラスでデストラクタを仮想化する理由は、派生クラスから選択肢を単に削除するためです。デストラクタはデフォルトで仮想になります。


2
通常、階層を定義するときに、基本クラスのポインター/参照を使用して派生オブジェクトを参照できるようにしたいので私はほとんどあなたに同意します。しかし、常にそうであるとは限らず、他の場合では、代わりに基本クラスのdtorを保護するだけで十分な場合があります。
j_random_hacker 2009

@j_random_hackerで保護しても、内部の誤った削除から保護されません
JaredPar

1
@JaredPar:そうですが、少なくともあなたは自分のコードに責任を持つことができます-難しいのは、クライアントコードがコードを爆発させないようにすることです。(同様に、データメンバーをプライベートにしても、内部コードがそのメンバーに対して愚かなことをするのを防ぐことはできません。)
j_random_hacker

@j_random_hacker、ブログ投稿で返信して申し訳ありませんが、このシナリオには本当に適しています。 blogs.msdn.com/jaredpar/archive/2008/03/24/...
JaredPar

@JaredPar:すばらしい投稿です。特に小売コードでの契約の確認については100%同意します。仮想DTORが必要ないことがわかっている場合があるということです。例:テンプレートディスパッチのタグクラス。サイズは0であり、継承を使用して専門化を示します。
j_random_hacker 2009

3

答えは単純です。仮想である必要があります。そうでなければ、基本クラスは完全な多態性クラスではありません。

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

上記の削除をお勧めしますが、基本クラスのデストラクタが仮想でない場合は、基本クラスのデストラクタのみが呼び出され、派生クラスのすべてのデータは削除されずに残ります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.