C ++で純粋な仮想デストラクタが必要なのはなぜですか?


154

仮想デストラクタの必要性を理解しています。しかし、なぜ純粋な仮想デストラクタが必要なのでしょうか。C ++の記事の1つで、著者は、クラスを抽象化するときに純粋な仮想デストラクタを使用すると述べています。

しかし、メンバー関数のいずれかを純粋な仮想関数にすることで、クラスを抽象化できます。

だから私の質問は

  1. デストラクタを本当に仮想化するのはいつですか?誰かが良いリアルタイムの例を挙げられますか?

  2. 抽象クラスを作成するときに、デストラクタも純粋に仮想化することをお勧めしますか?はいの場合、なぜですか?



14
@ Daniel-言及されたリンクは私の質問に答えません。純粋な仮想デストラクタに定義が必要な理由を説明します。私の質問は、なぜ純粋な仮想デストラクタが必要なのかということです。
マーク・

理由を探ろうとしていましたが、あなたはすでにここで質問をしました。
nsivakr 2010

回答:


119
  1. おそらく、純粋な仮想デストラクタが許可される本当の理由は、それらを禁止することは、言語に別のルールを追加することを意味し、純粋な仮想デストラクタを許可することによる悪影響はないため、このルールは不要です。

  2. いいえ、単純な古いバーチャルで十分です。

仮想メソッドのデフォルト実装を使用してオブジェクトを作成し、特定のメソッドをオーバーライドするように誰かに強制せずにそれを抽象化したい場合は、デストラクタを純粋な仮想にすることができます。あまり意味がありませんが可能です。

コンパイラは派生クラスの暗黙的なデストラクタを生成するため、クラスの作成者が生成しない場合、派生クラスは抽象的ではないことに注意してください。したがって、基本クラスに純粋な仮想デストラクタがあっても、派生クラスには何の違いもありません。基本クラスを抽象化するだけです(@kappaのコメントに感謝します)。

また、すべての派生クラスには特定のクリーンアップコードが必要であり、純粋な仮想デストラクタを使用してそれを記述するリマインダーを使用する必要があると想定することもできますが、これは不自然な(および強制されていない)ようです。

注:デストラクタはそれがあっても唯一の方法である純粋仮想持つインスタンス化は、(はい、純粋仮想関数は実装を持つことができる)のクラスを派生させるために実装を持っています。

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

13
「そうです、純粋仮想関数は実装を持つことができます」それからそれは純粋仮想ではありません。
GManNickG 2009

2
クラスを抽象化したい場合は、すべてのコンストラクタを保護する方が簡単ではないでしょうか?
bdonlan 2009

78
@GMan、あなたは間違っています。純粋な仮想であることは、派生クラスがこのメソッドをオーバーライドする必要があることを意味します。これは実装を持つことと直交しています。私のコードをチェックして、自分で確認しfoof::barたい場合はコメント化してください。
モッティ

15
@GMan:C ++ FAQ liteは、「純粋な仮想関数の定義を提供することは可能ですが、これは通常、初心者を混乱させ、後になるまで避けるのが最善であることに注意してください」と述べています。parashift.com/c++-faq-lite/abcs.html#faq-22.4 ウィキペディア(その正当性の要塞)も同様に述べています。ISO / IEC規格は同様の用語を使用していると思います(残念ながら、現在のところ私のコピーは機能しています)...混乱を招くので同意します。特に、定義を提供する場合、特に明確にしない限り、この用語を使用しません。周りの新しいプログラマー...
リアンダー

9
@Motti:ここで興味深いのは、純粋な仮想デストラクタを派生(およびインスタンス化)クラスで明示的にオーバーライドする必要がないことです。そのような場合、暗黙の定義が使用されます:)
kappa '27

33

抽象クラスに必要なのは、少なくとも1つの純粋な仮想関数だけです。すべての関数が実行します。しかし、たまたま、デストラクタはどのクラスにもあるものなので、候補として常に存在します。さらに、デストラクタを(単なる仮想ではなく)純粋な仮想にすることには、クラスを抽象化する以外の動作上の副作用はありません。そのため、多くのスタイルガイドでは、クラスが抽象的であることを示すために純粋な仮想デストラクターを一貫して使用することを推奨しています。


1
それでも、なぜ純粋なvirtaulデストラクタの実装を提供するのか。デストラクタを純粋に仮想化し、その実装を提供しないことで、何が問題になる可能性がありますか。基本クラスのポインタのみが宣言されているため、抽象クラスのデストラクタが呼び出されることはないと思います。
クリシュナオザ2014年

4
@Surfing:派生クラスのデストラクタは、そのデストラクタが純粋に仮想であっても、その基本クラスのデストラクタを暗黙的に呼び出すためです。そのため、実装がない場合、未定義の動作が発生します。
a.peganz 14

19

抽象基本クラスを作成する場合:

  • それはインスタンス化できない(うん、これは「抽象的」という用語と重複しています!)
  • しかし、仮想デストラクタの動作必要です(派生型へのポインタではなくABCへのポインタを持ち歩き、それらを介して削除する予定です)。
  • ただし、他のメソッドには他の仮想ディスパッチ動作は必要ありません(他にメソッドない可能性あります。コンストラクタ/デストラクタ/割り当てを必要とするが、それ以外は必要ない単純な保護された「リソース」コンテナを検討してください)。

...デストラクタを純粋に仮想し、その定義(メソッド本体)を提供することにより、クラスを抽象化するのが最も簡単です。

架空のABCについて:

インスタンス化できないことを保証します(クラス自体の内部でさえ、これがプライベートコンストラクターでは不十分な場合がある理由です)。デストラクタに必要な仮想動作が得られ、別のメソッドを見つけてタグ付けする必要がありません。 「仮想」としての仮想ディスパッチは必要ありません。


8

私が読んだ回答からあなたの質問まで、実際に純粋な仮想デストラクタを使用する正当な理由を推測できませんでした。たとえば、次の理由はまったく私を納得させません:

おそらく、純粋な仮想デストラクタが許可される本当の理由は、それらを禁止することは、言語に別のルールを追加することを意味し、純粋な仮想デストラクタを許可することによる悪影響はないため、このルールは不要です。

私の意見では、純粋な仮想デストラクタが役立つ可能性があります。たとえば、コードに2つのクラスmyClassAとmyClassBがあり、myClassBがmyClassAから継承するとします。Scott Meyersの著書「More Effective C ++」の項目33「非リーフクラスを抽象化する」で述べた理由により、myClassAとmyClassBが継承する抽象クラスmyAbstractClassを実際に作成することをお勧めします。これにより、抽象化が向上し、オブジェクトのコピーなどで発生するいくつかの問題が回避されます。

(クラスmyAbstractClassを作成する)抽象化プロセスでは、myClassAまたはmyClassBのメソッドが純粋な仮想メソッド(myAbstractClassが抽象であるための前提条件)になるための適切な候補ではない可能性があります。この場合、抽象クラスのデストラクタを純粋仮想として定義します。

以下、自分で書いたコードの具体例を示します。共通のプロパティを共有するNumerics / PhysicsParamsという2つのクラスがあります。したがって、抽象クラスIParamsを継承させます。この場合、純粋に仮想的な方法はまったくありませんでした。たとえば、setParameterメソッドは、すべてのサブクラスに対して同じ本体を持つ必要があります。私が持っていた唯一の選択は、IParamsのデストラクタを純粋に仮想化することでした。

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

1
私はこの使用法が好きですが、継承を「強制」する別の方法は、IParam他のコメントで指摘されているように、保護対象のコンストラクターを宣言することです。
2015年

4

既に実装およびテストされている派生クラスを変更せずに基本クラスのインスタンス化を停止する場合は、基本クラスに純粋な仮想デストラクタを実装します。


3

ここで、仮想デストラクタが必要場合と、純粋な仮想デストラクタが必要場合を説明します

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. Baseクラスのオブジェクトを直接作成できないようにするには、純粋な仮想デストラクタを使用しますvirtual ~Base() = 0。通常、少なくとも1つの純粋な仮想関数が必要ですvirtual ~Base() = 0。この関数として、を取り上げましょう 。

  2. 上記のことを必要としないときは、Derivedクラスオブジェクトの安全な破棄だけが必要です。

    Base * pBase = new Derived(); pBaseを削除します。純粋な仮想デストラクタは必要ありません。仮想デストラクタのみがその仕事をします。


2

あなたはこれらの答えを使って仮説に取り掛かっているので、わかりやすくするために、私はより単純な、より現実的な説明にしようとします。

オブジェクト指向設計の基本的な関係は、IS-AとHAS-Aの2つです。私はそれらを作りませんでした。それが彼らの呼び名です。

IS-Aは、特定のオブジェクトが、クラス階層でその上にあるクラスのオブジェクトであることを識別することを示します。果物クラスのサブクラスである場合、バナナオブジェクトは果物オブジェクトです。これは、果物クラスが使用できる場所ならどこでも、バナナが使用できることを意味します。ただし、反射的ではありません。特定のクラスが必要な場合、特定のクラスを基本クラスに置き換えることはできません。

Has-aは、オブジェクトが複合クラスの一部であり、所有権関係があることを示しています。これは、C ++では、それがメンバーオブジェクトであることを意味します。したがって、それ自体を破棄する前に、所有クラスがそのクラスを破棄するか、所有権を引き継ぐ責任があります。

これら2つの概念は、c ++のような多重継承モデルよりも単一継承言語で実現する方が簡単ですが、ルールは基本的に同じです。Fruitクラスポインターを受け取る関数にBananaクラスポインターを渡すなど、クラスIDがあいまいな場合に問題が発生します。

仮想関数は、第一に、ランタイムのものです。これは、実行中のプログラムで呼び出されるときに実行する関数を決定するために使用されるという点で、ポリモーフィズムの一部です。

virtualキーワードは、クラスIDについてあいまいな場合に関数を特定の順序でバインドするコンパイラディレクティブです。仮想関数は常に(私が知る限り)親クラスにあり、メンバー関数とその名前のバインドは最初にサブクラス関数で、次に親クラス関数で行う必要があることをコンパイラーに示します。

Fruitクラスには、デフォルトで「NONE」を返す仮想関数color()を含めることができます。Bananaクラスのcolor()関数は、「YELLOW」または「BROWN」を返します。

しかし、Fruitポインタを取る関数が、それに送信されたバナナクラスでcolor()を呼び出す場合、どのcolor()関数が呼び出されますか?関数は通常、Fruitオブジェクトに対してFruit :: color()を呼び出します。

99%の時間は意図したものではありません。しかし、Fruit :: color()が仮想として宣言されている場合、正しいcolor()関数が呼び出し時にFruitポインターにバインドされるため、オブジェクトに対してBanana:color()が呼び出されます。ランタイムは、Fruitクラス定義で仮想としてマークされているため、ポインターが指すオブジェクトをチェックします。

これは、サブクラスの関数をオーバーライドすることとは異なります。その場合、それが果物へのIS-Aポインターであることを知っているだけであれば、果物ポインターは果物::色()を呼び出します。

そこで、「純粋な仮想関数」というアイデアが浮かび上がります。純粋さはそれとは何の関係もないので、それはかなり残念なフレーズです。これは、基本クラスのメソッドが呼び出されないように意図されていることを意味します。実際、純粋な仮想関数を呼び出すことはできません。ただし、それでも定義する必要があります。関数シグネチャが存在している必要があります。多くのプログラマーは完全を期すために空の実装{}を作成しますが、そうでない場合はコンパイラーが内部で生成します。その場合、ポインターがFruitへのポインターであっても関数が呼び出されると、banana :: color()が呼び出されます。これは、color()の唯一の実装であるためです。

パズルの最後のピース:コンストラクタとデストラクタ。

純粋な仮想コンストラクタは完全に違法です。それだけです。

ただし、純粋な仮想デストラクタは、基本クラスインスタンスの作成を禁止する場合に機能します。基本クラスのデストラクタが純粋仮想の場合、サブクラスのみをインスタンス化できます。慣例では、これを0に割り当てます。

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

この場合、実装を作成する必要があります。コンパイラは、これがあなたのやっていることを知っており、正しく実行していることを確認します。または、コンパイルする必要のあるすべての関数にリンクできないことを強く訴えます。クラス階層をどのようにモデル化しているかについて正しい方向に進んでいない場合、エラーは混乱を招く可能性があります。

したがって、この場合、Fruitのインスタンスを作成することは禁止されていますが、Bananaのインスタンスを作成することは許可されています。

Bananaのインスタンスを指すFruitポインターの削除を呼び出すと、最初にBanana ::〜Banana()が呼び出され、次にFuit ::〜Fruit()が常に呼び出されます。何があっても、サブクラスデストラクタを呼び出すときは、基本クラスデストラクタが続く必要があるためです。

それは悪いモデルですか?設計段階ではより複雑ですが、実行時に正しいリンクが確実に実行され、アクセスされているサブクラスが正確に曖昧な場合にサブクラス関数が実行されます。

ジェネリックポインターもあいまいなポインターもない正確なクラスポインターのみを渡すようにC ++を作成する場合、仮想関数は実際には必要ありません。しかし、型の実行時の柔軟性が必要な場合(Apple Banana Orange ==> Fruitなど)、関数は冗長コードが少なくなり、より簡単で多用途になります。果物の種類ごとに関数を記述する必要がなくなり、すべての果物が独自の正しい関数でcolor()に応答することがわかります。

この長々とした説明が、混乱を招くのではなく、概念を固めることを願っています。よく見る良い例がたくさんあり、十分に見て実際にそれらを実行し、それらをいじると、あなたはそれを得るでしょう。


1

これは10年前のトピックです:)詳細については、「Effective C ++」の項目#7の最後の5段落をお読みください。


0

あなたは例を求めました、そして私は以下が純粋な仮想デストラクタの理由を提供すると思います。これがaであるかどうかについて返信をお待ちしています正当な理由ます...

誰もerror_base型をスローできないようにしたいのですが、例外型error_oh_shuckserror_oh_blast同じ機能を持っているので、2回書きたくありません。std::stringクライアントに公開されないようにするには、pImplの複雑さが必要であり、pImpl の使用にstd::auto_ptrはコピーコンストラクターが必要です。

パブリックヘッダーには、ライブラリがスローするさまざまなタイプの例外を区別するためにクライアントが使用できる例外仕様が含まれています。

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

そしてこれが共有実装です:

// 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();
}

プライベートに保たれたexception_stringクラスは、std :: stringを私のパブリックインターフェイスから隠します。

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

次に、私のコードは次のようなエラーをスローします。

#include "error.h"

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

用のテンプレートの使用はerror少し不必要です。次のようにクライアントにエラーをキャッチするように要求する代わりに、コードを少し節約します。

// client.cpp

#include <error.h>

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

0

多分別の実際のユースケースがあります私が実際に他の答えで見ることができない純粋な仮想デストラクタのがある:)

最初に、私はマークされた答えに完全に同意します。それは、純粋な仮想デストラクタを禁止すると、言語仕様で追加のルールが必要になるためです。しかし、それでもMarkが求めているのはユースケースではありません:)

まずこれを想像してください:

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

そして次のようなもの:

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

単に-私たちはインターフェースPrintableと、このインターフェースで何かを保持するいくつかの「コンテナ」を持っています。ここで、print()メソッドが純粋に仮想である理由は明らかです。本体を含めることもできますが、デフォルトの実装がない場合は、純粋な仮想が理想的な「実装」です(= "子孫クラスによって提供される必要があります")。

そして、印刷用ではなく破壊用であることを除いて、まったく同じことを想像してみてください。

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

また、同様のコンテナがある可能性があります。

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

それは私の実際のアプリケーションからの単純化されたユースケースです。ここでの唯一の違いは、「通常」の代わりに「特別な」メソッド(デストラクタ)が使用されたことです。print()。しかし、それが純粋な仮想である理由は同じです-メソッドのデフォルトコードはありません。少し混乱するのは、デストラクタが効果的に存在しなければならず、コンパイラが実際に空のコードを生成するという事実である可能性があります。しかし、プログラマーの観点からは、純粋な仮想性は依然として「デフォルトのコードがないため、派生クラスによって提供される必要がある」ことを意味します。

ここでは大きなアイデアはないと思います。純粋な仮想性がデストラクタに対しても本当に均一に機能することをさらに説明します。



-2

デストラクターを仮想化しないと、コンパイラーは基本クラスのコンテンツのみを破壊し、n派生クラスはすべて変更されないままになるため、デストラクターを仮想化する必要があります。基本クラスを除くクラス。


-1:問題は、デストラクタが仮想であるべき理由ではありません。
トルバドール

さらに、特定の状況では、デストラクタは正しい破壊を達成するために仮想である必要はありません。仮想デストラクタはdelete、実際にはその派生クラスを指しているのに、基本クラスへのポインタを呼び出す場合にのみ必要です。
CygnusX1 2013年

あなたは100%正解です。これは、C ++プログラムでのリークとクラッシュの最大の原因の1つであり、過去に存在しています。3番目は、nullポインターを使用して、配列の境界を超えて処理を行うことです。非仮想基本クラスデストラクタは、サブクラスデストラクタが仮想としてマークされていない場合は完全にバイパスして、ジェネリックポインタで呼び出されます。サブクラスに属している動的に作成されたオブジェクトがある場合、それらは削除の呼び出しで基本デストラクタによって回復されません。あなたは大丈夫、それからBLUURRKに沿って抱きしめています!(どこも見つけにくい。)
Chris Reid
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.