C ++で純粋な仮想デストラクタが必要なのはなぜですか?
-
10-07-2019 - |
質問
仮想デストラクタの必要性を理解しています。しかし、なぜ純粋な仮想デストラクタが必要なのでしょうか? C ++の記事の1つで、著者は、クラスを抽象化する場合に純粋な仮想デストラクタを使用することを述べています。
ただし、メンバー関数のいずれかを純粋仮想として作成することにより、クラスを抽象化できます。
だから私の質問は
-
デストラクタを純粋に仮想化するのはいつですか?誰かが良いリアルタイムの例を挙げることができますか?
-
抽象クラスを作成するとき、デストラクタも純粋仮想にするのは良い習慣ですか?はいの場合、それはなぜですか?
解決
-
おそらく、純粋な仮想デストラクタが許可される本当の理由は、それらを禁止することは言語に別のルールを追加することを意味し、純粋な仮想デストラクタを許可しても悪影響が生じないためです。
-
いいえ、プレーンな古い仮想マシンで十分です。
仮想メソッドのデフォルト実装を使用してオブジェクトを作成し、特定のメソッドを強制的に上書きせずに抽象化する場合、デストラクタを純粋な仮想にすることができます。あまり意味はありませんが、可能です。
コンパイラは派生クラスの暗黙的なデストラクタを生成するため、クラスの作成者がそうしない場合、派生クラスは抽象的ではない ことに注意してください。したがって、基本クラスに純粋な仮想デストラクタがあっても、派生クラスに違いはありません。基本クラスのみを抽象化します( @kappa のコメントに感謝します)。
また、すべての派生クラスはおそらく特定のクリーンアップコードを必要とし、純粋な仮想デストラクタを使用して1つを記述する必要があると想定するかもしれませんが、これは不自然な(そして強制されていない)ようです。
注:デストラクタは、純粋な仮想 であっても、派生クラスをインスタンス化するための実装を持つ必要がある唯一の方法です(はい、純粋な仮想関数は実装を持つことができます。)
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.
};
他のヒント
抽象クラスに必要なのは、少なくとも1つの純粋仮想関数だけです。どの機能でも実行できます。しかし、実際には、デストラクタは any クラスが持つものなので、常に候補として存在します。さらに、デストラクタを(単なる仮想とは対照的に)純粋な仮想にすることは、クラスを抽象化すること以外の動作上の副作用はありません。そのため、多くのスタイルガイドは、純粋な仮想デストラクターを一貫して使用して、クラスが抽象的であることを示すことを推奨しています。
抽象基本クラスを作成する場合:
- インスタンス化できない(ええ、これは用語「抽象」と冗長です!)
- しかし、仮想デストラクタの動作が必要(派生型へのポインタではなく、ABCへのポインタを持ち運び、削除するつもりです)
- しかし、他のメソッドのその他の仮想ディスパッチ動作は必要ありません(他のメソッドはないかもしれません ?コンストラクタを必要とする単純な保護された「リソース」コンテナを考えてください/ destructor / assignmentですが、他にはあまりありません)
...デストラクタを純粋な仮想 にしてその定義(メソッド本体)を提供することにより、クラスを抽象化するのが最も簡単です。
仮想ABCの場合:
インスタンス化できないことを保証します(クラス自体の内部でさえ、これがプライベートコンストラクターでは十分でない理由です)、デストラクタに必要な仮想動作を取得し、別のものを見つけてタグを付ける必要はありません" virtual"としての仮想ディスパッチを必要としないメソッド。
私があなたの質問に読んだ答えから、純粋な仮想デストラクタを実際に使用する正当な理由を推測できませんでした。たとえば、次の理由は私をまったく納得させません:
おそらく、純粋な仮想デストラクタが許可されている本当の理由は、それらを禁止することは言語に別のルールを追加することを意味し、純粋な仮想デストラクタを許可することによって悪影響が生じることはないため、このルールの必要がないことです。
私の意見では、純粋な仮想デストラクタが役立つ可能性があります。たとえば、コードに2つのクラスmyClassAとmyClassBがあり、myClassBがmyClassAを継承すると仮定します。 Scott Meyersの著書「More Effective C ++」、Item 33「Making non-leaf classes abstract」で言及されている理由により、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;
};
既に実装およびテスト済みの派生クラスに変更を加えずに基本クラスのインスタンス化を停止する場合は、基本クラスに純粋な仮想デストラクタを実装します。
ここで、仮想デストラクタが必要な場合と純粋な仮想デストラクタが必要な場合
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
}
-
Baseクラスのオブジェクトを誰も直接作成できないようにするには、純粋な仮想デストラクタ
virtual〜Base()= 0
を使用します。通常、少なくとも1つの純粋仮想関数が必要です。この関数として、virtual〜Base()= 0
を使用してみましょう。 -
上記のものが必要ない場合は、Derivedクラスオブジェクトを安全に破棄する必要があるのは
Base * pBase = new Derived(); pBaseを削除します。 純粋な仮想デストラクタは必要ありません。仮想デストラクタのみがジョブを実行します。
これらの答えで仮説を立てているので、わかりやすくするために、より単純で現実的な説明をしようとします。
オブジェクト指向設計の基本的な関係は2つです。 IS-AおよびHAS-A。私はそれらを作りませんでした。それが彼らの名前です。
IS-Aは、特定のオブジェクトがクラス階層内でその上にあるクラスであると識別することを示します。バナナオブジェクトは、フルーツクラスのサブクラスである場合、フルーツオブジェクトです。これは、フルーツクラスを使用できる場所であればどこでも、バナナを使用できることを意味します。ただし、再帰的ではありません。特定のクラスが必要な場合、特定のクラスを基本クラスに置き換えることはできません。
Has-aは、オブジェクトが複合クラスの一部であり、所有権関係があることを示しました。 C ++では、それはメンバーオブジェクトであり、それ自体を破棄する前に、所有するクラスがそれを破棄するか所有権を引き継ぐという責任があります。
これらの2つの概念は、c ++のような多重継承モデルよりも単一継承言語で簡単に実現できますが、ルールは基本的に同じです。複雑さは、バナナクラスポインターをFruitクラスポインターを取る関数に渡すなど、クラスアイデンティティがあいまいな場合に発生します。
仮想関数は、第一に実行時のものです。ポリモーフィズムの一部であり、実行中のプログラムで呼び出されるときに実行する関数を決定するために使用されます。
virtualキーワードは、クラスアイデンティティについてあいまいな点がある場合に特定の順序で関数をバインドするコンパイラディレクティブです。仮想関数は常に(私の知る限り)親クラス内にあり、名前に対するメンバー関数のバインドは最初にサブクラス関数で、次に親クラス関数で行う必要があることをコンパイラーに示します。
Fruitクラスには、「NONE」を返す仮想関数color()を含めることができます。デフォルトで。 Bananaクラスのcolor()関数は&quot; YELLOW&quot;を返します。または&quot; BROWN&quot;。
しかし、Fruitポインターを受け取る関数が、それに送信されたBananaクラスでcolor()を呼び出す場合、どのcolor()関数が呼び出されますか? この関数は通常、Fruitオブジェクトに対してFruit :: color()を呼び出します。
それは、時間の99%が意図したものではないということです。 しかし、Fruit :: color()がvirtualと宣言されている場合、オブジェクトのBanana:color()が呼び出されます。これは、呼び出し時に正しいcolor()関数がFruitポインターにバインドされるためです。 ランタイムは、Fruitクラス定義で仮想とマークされているため、ポインターが指すオブジェクトを確認します。
これは、サブクラスの関数をオーバーライドすることとは異なります。その場合 Fruitポインタは、FruitへのIS-Aポインタであることがわかっている場合にのみFruit :: color()を呼び出します。
では、「純粋な仮想関数」というアイデアに移りました。出てきます。 純度はそれとは何の関係もないので、それはかなり不幸なフレーズです。これは、基本クラスのメソッドが呼び出されないことを意図していることを意味します。 実際、純粋な仮想関数を呼び出すことはできません。ただし、定義する必要があります。関数シグネチャが存在する必要があります。多くのコーダーは完全を期すために空の実装{}を作成しますが、そうでない場合はコンパイラーが内部で実装します。ポインターがFruitであっても関数が呼び出される場合、Banana :: color()はcolor()の唯一の実装なので呼び出されます。
パズルの最後のピース、コンストラクタとデストラクタ。
純粋な仮想コンストラクタは完全に違法です。それはちょうど外です。
ただし、基本クラスインスタンスの作成を禁止する場合は、純粋な仮想デストラクタが機能します。基本クラスのデストラクターが純粋仮想である場合、サブクラスのみをインスタンス化できます。 慣例では、0に割り当てます。
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
この場合、実装を作成する必要があります。コンパイラはこれがあなたがしていることだと知っており、
例を求めましたが、純粋な仮想デストラクタの理由は次のとおりだと思います。これが良い理由であるかどうかについての返信をお待ちしています...
error_base
タイプを誰にも投げられたくないのですが、例外タイプ error_oh_shucks
と error_oh_blast
は同じ機能を持ち、私は二度書きたくありません。 pImplの複雑さは、 std :: string
をクライアントに公開しないようにするために必要であり、 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();
}
privateに保持されている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>&) {
}
たぶん、純粋な仮想デストラクタの別の実際のユースケースがありますが、実際には他の回答では見ることができません:
最初は、マークされた答えに完全に同意します。純粋な仮想デストラクタを禁止するには、言語仕様に追加のルールが必要だからです。しかし、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
といくつかの&quot; container&quot;があります。このインターフェースで何かを保持しています。ここで print()
メソッドが純粋な仮想である理由は非常に明確だと思います。何らかのボディを持つことができますが、デフォルトの実装がない場合、純粋な仮想は理想的な「実装」です。 (=&quot;子孫クラスによって提供される必要があります&quot;)。
そして今では、印刷用ではなく破壊用であることを除いて、まったく同じことを想像してください:
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()
。しかし、それが純粋な仮想である理由はまだ同じです-メソッドのデフォルトコードはありません。
少し混乱するのは、デストラクタが効果的に存在しなければならず、コンパイラが実際に空のコードを生成するという事実です。しかし、プログラマーの観点から見ると、純粋な仮想性とは、「デフォルトのコードはありません。派生クラスによって提供される必要があります」という意味です。
ここでは大きなアイディアではなく、純粋な仮想性がデストラクタに対しても均一に機能することを説明するだけです。
これは10年前のトピックです:) &quot; Effective C ++&quot;の項目#7の最後の5段落を読んでください。詳細については、「時折、クラスに純粋な仮想デストラクタを与えると便利な場合があります...」から始まります。
1)派生クラスにクリーンアップを要求する場合。これはまれです。
2)いいえ。ただし、仮想化する必要があります。
デストラクタを仮想化する必要があるのは、デストラクタを仮想化しない場合、コンパイラは基本クラスのコンテンツを破棄するだけであり、nすべての派生クラスは変更されないままであり、bacuseコンパイラはデストラクタを呼び出さない基本クラスを除く他のクラスの。