仮想デストラクタの必要性を理解しています。しかし、なぜ純粋な仮想デストラクタが必要なのでしょうか? C++の記事の1つで、著者は、クラスを抽象化する場合に純粋な仮想デストラクタを使用すると述べています。
ただし、メンバー関数のいずれかを純粋仮想として作成することにより、クラスを抽象化できます。
だから私の質問は
いつデストラクタを純粋な仮想にするのですか?誰かがリアルタイムの良い例を挙げることができますか?
抽象クラスを作成するとき、デストラクタも純粋仮想にするのは良い習慣ですか?はいの場合は、なぜですか?
おそらく、純粋な仮想デストラクタが許可される本当の理由は、それらを禁止することは言語に別のルールを追加することを意味することであり、純粋な仮想デストラクタを許可することによって悪影響が生じることはないため、このルールは必要ないからです。
いいえ、単純な古い仮想マシンで十分です。
仮想メソッドのデフォルト実装を使用してオブジェクトを作成し、誰にもspecificメソッドを強制的にオーバーライドせずに抽象化する場合、デストラクタを作成できます。純粋な仮想。あまり意味はありませんが、可能です。
コンパイラは派生クラスに対して暗黙的なデストラクタを生成するため、クラスの作成者がそうしない場合、派生クラスはnot抽象になります。したがって、基本クラスに純粋な仮想デストラクタがあっても、派生クラスに違いはありません。基本クラスを抽象化するだけです( @ kappa のコメントに感謝します)。
また、すべての派生クラスはおそらく特定のクリーンアップコードを必要とし、純粋な仮想デストラクタを使用して1つを記述する必要があると想定するかもしれませんが、これは人為的な(そして強制されていない)ようです。
注:デストラクタは、ispure virtualhasは、派生クラスをインスタンス化するために実装する必要があります(はい、純粋な仮想関数は実装を持つことができます)。
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クラスが持っているものなので、常に候補として存在します。さらに、デストラクタを(単なる仮想とは対照的に)純粋な仮想にすることは、クラスを抽象化すること以外の動作上の副作用はありません。そのため、多くのスタイルガイドは、純粋な仮想デストラクターを一貫して使用して、クラスが抽象的であることを示すことを推奨しています。
抽象基本クラスを作成する場合:
...デストラクタを純粋仮想andにして定義(メソッド本体)を提供することにより、クラスを抽象化するのが最も簡単です。
架空のABCの場合:
インスタンス化できないことを保証します(クラス自体の内部です。これがプライベートコンストラクターでは十分でない理由です)。 「仮想」として仮想ディスパッチする必要はありません。
私があなたの質問に読んだ答えから、純粋な仮想デストラクタを実際に使用する正当な理由を推測できませんでした。たとえば、次の理由はまったく私を納得させません。
おそらく、純粋な仮想デストラクタが許可される本当の理由は、それらを禁止することは言語に別のルールを追加することを意味することであり、純粋な仮想デストラクタを許可することによって悪影響が生じることはないため、このルールは必要ないからです。
私の意見では、純粋な仮想デストラクタが役立つ可能性があります。たとえば、コードにmyClassAとmyClassBの2つのクラスがあり、myClassBがmyClassAを継承すると仮定します。 Scott Meyersの著書「More Effective C++」、Item 33「Make 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;
};
既に実装およびテスト済みの派生クラスを変更せずに基本クラスのインスタンス化を停止する場合は、基本クラスに純粋な仮想デストラクタを実装します。
ここで、virtual destructorが必要なときとpure virtual destructorが必要なときを伝えたい
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を削除します。純粋な仮想デストラクタは必要ありません。仮想デストラクタのみがジョブを実行します。
あなたはこれらの答えで仮説を立てているので、わかりやすくするために、よりシンプルで、より現実的な説明をしようとします。
オブジェクト指向設計の基本的な関係は、IS-AとHAS-Aの2つです。私はそれらを作りませんでした。それが彼らの名前です。
IS-Aは、特定のオブジェクトがクラス階層内でその上にあるクラスのものとして識別されることを示します。バナナオブジェクトは、フルーツクラスのサブクラスである場合、フルーツオブジェクトです。これは、フルーツクラスを使用できる場所であればどこでも、バナナを使用できることを意味します。ただし、再帰的ではありません。特定のクラスが必要な場合、特定のクラスを基本クラスに置き換えることはできません。
Has-aは、オブジェクトが複合クラスの一部であり、所有権関係があることを示しました。 C++では、それはメンバーオブジェクトであり、それ自体を破棄する前にそれを破棄するか所有権を渡すのは所有クラスにあるということです。
これらの2つの概念は、c ++のような多重継承モデルよりも単一継承言語で簡単に実現できますが、ルールは基本的に同じです。複雑さは、バナナクラスポインターをFruitクラスポインターを取る関数に渡すなど、クラスアイデンティティがあいまいな場合に発生します。
仮想機能は、第一に、実行時のものです。ポリモーフィズムの一部であり、実行中のプログラムで呼び出されるときに実行する関数を決定するために使用されます。
仮想キーワードは、クラスのアイデンティティにあいまいさがある場合に特定の順序で関数をバインドするコンパイラ指令です。仮想関数は常に(私の知る限り)親クラス内にあり、名前へのメンバー関数のバインドは最初にサブクラス関数で、次に親クラス関数で行う必要があることをコンパイラーに示します。
Fruitクラスには、デフォルトで「NONE」を返す仮想関数color()を含めることができます。 Bananaクラスのcolor()関数は、「YELLOW」または「BROWN」を返します。
しかし、Fruitポインターを取る関数がそれに送られたBananaクラスでcolor()を呼び出す場合、どのcolor()関数が呼び出されますか?この関数は通常、Fruitオブジェクトに対してFruit :: color()を呼び出します。
それは時間の99%が意図したものではないでしょう。しかし、Fruit :: color()がvirtualとして宣言されている場合、オブジェクトのBanana:color()が呼び出されます。これは、呼び出し時に正しいcolor()関数がFruitポインタにバインドされるためです。ランタイムは、Fruitクラス定義で仮想とマークされているため、ポインターが指すオブジェクトを確認します。
これは、サブクラスの関数をオーバーライドすることとは異なります。その場合、フルーツへのポインタがIS-Aであることがわかっている場合、フルーツポインタはFruit :: color()を呼び出します。
そこで、「純粋な仮想機能」というアイデアが浮上しました。純度はそれとは何の関係もないので、それはかなり不幸なフレーズです。これは、基本クラスのメソッドが呼び出されないことを意図していることを意味します。実際、純粋な仮想関数を呼び出すことはできません。ただし、定義する必要があります。関数シグネチャが存在する必要があります。多くのコーダーは完全を期すために空の実装{}を作成しますが、そうでない場合はコンパイラーが内部で実装します。その場合、ポインターがFruitであっても関数が呼び出されると、Banana :: color()がcolor()の唯一の実装であるため呼び出されます。
パズルの最後のピース、コンストラクタとデストラクタ。
純粋な仮想コンストラクタは完全に違法です。それはちょうど外です。
ただし、基本クラスインスタンスの作成を禁止する場合は、純粋な仮想デストラクタが機能します。基本クラスのデストラクターが純粋仮想である場合、サブクラスのみをインスタンス化できます。規則では、0に割り当てます。
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
この場合、実装を作成する必要があります。コンパイラは、これがあなたがしていることを知っており、あなたがそれを正しく行うことを確認します。または、コンパイルに必要なすべての関数にリンクできないと激しく文句を言います。クラス階層をどのようにモデリングしているかに関して正しい軌道に乗っていない場合、エラーは混乱を招く可能性があります。
この場合、フルーツのインスタンスを作成することは禁止されていますが、バナナのインスタンスを作成することは許可されています。
Bananaのインスタンスを指すFruitポインターの削除呼び出しは、最初にBanana ::〜Banana()を呼び出し、次にFuit ::〜Fruit()を常に呼び出します。何であれ、サブクラスデストラクタを呼び出すときは、基本クラスデストラクタが従う必要があるためです。
それは悪いモデルですか?はい、設計段階ではより複雑になりますが、実行時に正しいリンクが実行され、アクセスされるサブクラスが正確に曖昧な場所でサブクラス関数が実行されることを保証できます。
ジェネリックポインターもあいまいなポインターを使用せずに正確なクラスポインターのみを渡すようにC++を記述する場合、仮想関数は実際には必要ありません。ただし、タイプの実行時の柔軟性が必要な場合(Apple Banana Orange ==> Fruitのように)、冗長性の少ないコードで関数がより簡単で多用途になります。果物の種類ごとに関数を作成する必要はなくなり、すべての果物が正しい関数でcolor()に応答することがわかります。
この長々とした説明が、物事を混乱させるのではなく、概念を固めることを願っています。見て、そして十分に見て、実際にそれらを実行し、それらを台無しにする多くの良い例があります。
あなたは例を求めましたが、純粋な仮想デストラクタの理由は次のとおりだと思います。これがgood理由かどうかについての返信を楽しみにしています...
誰かがerror_base
タイプをスローできるようにしたくないのですが、例外タイプerror_oh_shucks
とerror_oh_blast
は同じ機能を持っているので、2回書きたくありません。 std::string
をクライアントに公開しないようにするには、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>&) {
}
たぶん、別のREAL USE-CASE純粋な仮想デストラクタがありますが、実際には他の答えでは見ることができません:)
最初は、マークされた答えに完全に同意します。純粋な仮想デストラクタを禁止するには、言語仕様に追加のルールが必要だからです。しかし、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()
の代わりに「特別な」メソッド(デストラクタ)が使用されたことです。しかし、それが純粋な仮想である理由はまだ同じです-メソッドのデフォルトコードはありません。少し混乱するのは、デストラクタが効果的に存在しなければならず、コンパイラが実際に空のコードを生成するという事実です。しかし、プログラマーの観点から見ると、純粋な仮想性は依然として「デフォルトコードはありません。派生クラスによって提供される必要があります」という意味です。
ここでは大きなアイデアではなく、純粋な仮想性が実際に均一に機能することをさらに説明します-デストラクタについても同様です。
これは10年前のトピックです:)詳細については、「効果的なC++」本の項目#7の最後の5段落を読んでください。