C ++で仮想メソッドを呼び出すことは禁止されていますが、非常に役立つ場合がいくつかあります。
次の状況を考えてみましょう-親クラスと子クラスのペア。親コンストラクターは、特別な方法で初期化する必要があるため、子クラスをインスタンス化する必要があります。DerivedParentがDerivedChildを使用するように、親と子の両方を派生させることができます。
ただし、問題があります。DerivedParent::ctorからのParent:: ctor呼び出しでは、基本クラスにChildではなくDerivedChildのインスタンスが必要であるためです。ただし、これには、仮想メソッドを何らかの方法で呼び出す必要があります。これは禁止されています。私はこのようなことについて話している:
class Child
{
public:
virtual std::string ToString() { return "Child"; }
};
class DerivedChild : public Child
{
public:
std::string ToString() { return "DerivedChild"; }
};
class Parent
{
protected:
Child * child;
virtual Child * CreateChild() { return new Child(); }
public:
Parent() { child = CreateChild(); }
Child * GetChild() { return child; }
};
class DerivedParent : public Parent
{
protected:
Child * CreateChild() { return new DerivedChild(); }
};
int main(int argc, char * argv[])
{
DerivedParent parent;
printf("%s\n", parent.GetChild()->ToString().c_str());
getchar();
return 0;
}
実際の例を見てみましょう。WinApiのウィンドウのラッパーを作成するとします。基本コントロールクラスは、クラスを登録し、ウィンドウ(RegisterClassExやCreateWindowExなど)をインスタンス化して、適切に設定する必要があります(たとえば、ウィンドウ構造にクラスインスタンスの追加データがあるようにクラスを登録し、すべてのコントロールに汎用WndProcを設定します)。 ; this
SetWindowLongPtrなどによる参照を入れます)
一方、派生クラスは、スタイルと拡張スタイル、ウィンドウのクラス名などを指定できる必要があります。
コントロールのコンストラクターでウィンドウのインスタンスを構築することが履行される契約である場合、ctorでVMを使用する以外の解決策はありません(機能しないもの)。
考えられる回避策:
- 静的ポリモーフィズム(例:クラスDerived:public Base)を使用しますが、Derivedから派生させたい場合は、機能しません。
- 可能であれば、派生コンストラクターからベースコンストラクターにラムダを渡します。これは機能しますが、完全なハードコアソリューションです。
- 派生コンストラクターからベースコンストラクターに大量のパラメーターを渡します。これは機能しますが、エレガントでも使いやすいものでもありません。
私は個人的にどちらも好きではありません。好奇心から、DelphiのVCLで問題がどのように解決されるかを確認しました。基本クラスは、仮想のCreateParamsを呼び出します(Delphiは、このような呼び出しを許可し、安全であることを保証します。作成時にクラスフィールドは0に初期化されます。 )。
この言語の制限をどのように克服できますか?
編集:回答に応じて:
派生コンストラクターからCreateChildを呼び出します。すでにCreateChildを定義する必要があるため、これは段階的な手順です。基本クラスにprotected:void init()関数を追加して、そのような初期化をカプセル化しておくことができます。
それは動作しますが、オプションではありません-有名なC ++ FAQを引用します:
最初のバリエーションは最初は最も単純ですが、実際にオブジェクトを作成したいコードには、プログラマーの自己規律が少し必要です。これは、実際には運命にあることを意味します。真剣に、この階層のオブジェクトを実際に作成する場所が1つか2つしかない場合、プログラマーの自己規律は非常にローカライズされており、問題を引き起こすことはありません。
CRTPを使用します。基本クラスをテンプレートにし、子にDerivedTypeを提供させ、その方法でコンストラクターを呼び出します。この種の設計では、仮想機能を完全に排除できる場合があります。
基本クラスとその直系の子孫に対しては1回しか機能しないため、これはオプションではありません。
子ポインタを、たとえばファクトリ関数へのコンストラクタ引数にします。
これは、これまでのところ、最良の解決策であり、ベースコンストラクターにコードを挿入します。私の場合は、ベースコンストラクターをパラメーター化して子孫から値を渡すことができるため、これを行う必要はありません。しかし、実際には機能します。
親を返す前に、ファクトリ関数テンプレートを使用して適切なタイプの子ポインタを生成します。これにより、クラステンプレートの複雑さが解消されます。タイプ特性パターンを使用して、子クラスと親クラスをグループ化します。
ええ、しかしそれはいくつかの欠点があります:
- 私のクラスから派生した誰かが、彼のctorを公開し、セキュリティ対策をバイパスする可能性があります。
- 参照を使用できなくなり、スマートポインタを使用する必要があります。