次のコードがあるとします。
class BaseMember
{
};
class DerivedMember : public BaseMember
{
};
class Base
{
private:
BaseMember* mpMember;
protected:
virtual BaseMember* initializeMember(void)
{
return new BaseMember[1];
}
virtual void cleanupMember(BaseMember* pMember)
{
delete[] pMember;
}
public:
Base(void)
: mpMember(NULL)
{
}
virtual ~Base(void)
{
cleanupMember(mpMember);
}
BaseMember* getMember(void)
{
if(!mpMember)
mpMember = initializeMember();
return mpMember;
}
};
class Derived : public Base
{
protected:
virtual BaseMember* initializeMember(void)
{
return new DerivedMember;
}
virtual void cleanupMember(BaseMember* pMember)
{
delete pMember;
}
};
Base と BaseMember は API の一部であり、サンプル コードで Derived と DerivedMember を介して行われるように、その API のユーザーによってサブクラス化される場合があります。
Base は、その仮想ファクトリ関数 initializeMember() への呼び出しによって mpBaseMember を初期化します。これにより、派生クラスはファクトリ関数をオーバーライドして、BaseMember インスタンスではなく DerivedMember インスタンスを返すことができます。
ただし、基本クラスのコンストラクター内から仮想関数を呼び出すと、派生クラスのオーバーライドではなく基本実装が呼び出されます。そのため、最初にアクセスされるまで、mpMember の初期化を待機しています (もちろん、基本クラスと、それ自体がさらに派生する可能性のある派生クラスは、内部からそのメンバーにアクセスすることが許可されていないことを意味します)。コンストラクタ)。
問題は次のとおりです。基本デストラクタ内から仮想メンバー関数を呼び出すと、派生クラスのオーバーライドではなく、その関数の基本クラスの実装が呼び出されます。つまり、基本クラスのデストラクタ内から cleanupMember() を単純に呼び出すことはできません。これは、initializeMember() の派生実装が初期化したものを正しくクリーンアップできない可能性がある基本クラスの実装を呼び出すためです。たとえば、基本クラスと派生クラスは互換性のないアロケーターを使用する可能性があり、混合すると未定義の動作が発生する可能性があります (コード例のように、派生クラスは new を介してメンバーを割り当てますが、基本クラスは delete[] を使用して割り当てを解除します)。 .
だから私の質問は、どうすればこの問題を解決できますか? 私が思いついたのは、a) API のユーザーは、Derived インスタンスが破棄される前に、何らかのクリーンアップ関数を明示的に呼び出す必要があるということです。それは忘れられがちです。b) (ほとんどの) 派生クラスのデストラクタは、基本クラスによって初期化がトリガーされたものをクリーンアップするために、クリーンアップ関数を呼び出す必要があります。所有権の責任が混同されているため、これは見苦しく、適切に設計されていません。基本クラスは割り当てをトリガーしますが、派生クラスは割り当て解除をトリガーする必要があります。これは非常に直感に反しており、派生クラスの作成者は、その情報を見つけるのに十分なAPIドキュメント。ドキュメントを徹底的に読むためにユーザーの記憶や信頼性に頼るよりも、確実な方法でこれを行いたいと思っています。
代替アプローチはありますか?
注: 基本クラスのコンパイル時には派生クラスが存在しない可能性があるため、ここでは静的ポリモーフィズムはオプションではありません。