RTTI と dynamic_cast<> を使用せずに C++ で二重ディスパッチを正しく処理する方法と、クラス階層が拡張可能なソリューション、つまり基本クラスをさらに派生させることができ、その定義/実装がそれについて知る必要がありますか?
仕方がないと思いますが、間違っていることが証明されてうれしいです:)
5 に答える
C ++の「ビジターパターン」は、多くの場合、ダブルディスパッチと同じです。RTTIまたはdynamic_castsを使用しません。
この質問への回答も参照してください。
最初に認識すべきことは、二重 (またはそれ以上の順序) ディスパッチはスケーリングしないということです。単一のディスパッチとタイプでは、関数n
が必要です。n
二重発送n^2
の場合等に。この問題をどのように処理するかによって、二重ディスパッチの処理方法が部分的に決まります。明らかな解決策の 1 つは、閉じた階層を作成して派生型の数を制限することです。その場合、ビジター パターンのバリアントを使用して、ダブル ディスパッチを簡単に実装できます。階層を閉じない場合、いくつかの可能なアプローチがあります。
すべてのペアが関数に対応すると主張する場合は、基本的に次のものが必要です。
std::map<std::pair<std::type_index, std::type_index>, void (*)(Base const& lhs, Base const& rhs)>
dispatchMap;
(必要に応じて関数シグネチャを調整します。) また、n^2
関数を実装して、dispatchMap
. (ここでは、フリー関数を使用していると仮定しています。それらを他のクラスではなくいずれかのクラスに配置する論理的な理由はありません。)その後、次のように呼び出します。
(*dispatchMap[std::make_pair( std::type_index( typeid( obj1 ) ), std::type_index( typeid( obj2 ) )])( obj1, obj2 );
(明らかに、それを関数にラップする必要があります。コード全体に散らばらせたいようなものではありません。)
マイナーな変形は、特定の組み合わせのみが合法であると言うことです。この場合、 で を使用
find
してdispatchMap
、探しているものが見つからない場合にエラーを生成できます。(多くのエラーが予想されます。) 何らかのデフォルトの動作を定義できる場合は、同じ解決策を使用できます。
100% 正しく実行したい場合は、一部の関数で中間クラスとそのすべての派生物を処理できるようにするには、ある種のより動的な検索と、オーバーロードの解決を制御するための順序付けが必要です。たとえば、次のように考えてください。
Base
/ \
/ \
I1 I2
/ \ / \
/ \ / \
D1a D1b D2a D2b
f(I1, D2a)
とがある場合はf(D1a, I2)
、どちらを選択する必要があります。最も簡単な解決策は、(オブジェクトへのポインターによって決定される) 呼び出すことができる最初のものを選択しdynamic_cast
、手動で挿入の順序を管理して、必要なオーバーロード解決を定義する線形検索です。ただし、関数を使用n^2
すると、これはかなり急速に遅くなる可能性があります。順序付けがあるので、 を使用できるはずですstd::map
が、順序付け関数を実装するのは明らかに自明ではありません (そして、依然としてdynamic_cast
あらゆる場所で使用する必要があります)。
すべてのことを考慮して、私の提案は、二重ディスパッチを小さくて閉じた階層に制限し、訪問者パターンのいくつかのバリアントに固執することです。
最初の問題は簡単です。dynamic_cast
実行時チェックと型キャストです。前者には RTTI が必要ですが、後者には必要ありません。RTTI を必要とせずに同じことを行う機能で dynamic_cast を置き換えるために必要なことは、実行時に型をチェックする独自のメソッドを用意することだけです。これを行うために必要なのは、それがどのタイプであるか、またはそれが準拠しているより具体的なインターフェイス (列挙型、整数 ID、さらには文字列である可能性があります) の何らかの識別を返す単純な仮想関数だけです。static_cast
キャストについては、ランタイム チェックを自分で実行し、キャスト先の型がオブジェクトの階層内にあることを確認したら、安全に実行できます。したがって、これにより、「完全な」機能をエミュレートするという問題が解決されますdynamic_cast
組み込みの RTTI を必要とせずに。別のより複雑な解決策は、独自の RTTI システムを作成することです (Matthieu が言及した LLVM のようないくつかのソフトウェアで行われているように)。
2番目の問題は大きな問題です。拡張可能なクラス階層でうまくスケーリングする二重ディスパッチ メカニズムを作成する方法。それは難しいです。コンパイル時 (静的ポリモーフィズム) では、これは関数のオーバーロード (および/またはテンプレートの特殊化) を使用して非常にうまく行うことができます。実行時には、これははるかに困難です。私の知る限り、唯一の解決策は、Konrad が述べたように、関数ポインタ (またはその性質のもの) のディスパッチ テーブルを保持することです。私の意見では、静的ポリモーフィズムを使用し、ディスパッチ関数をカテゴリ (関数シグネチャなど) に分割することで、型の安全性に違反する必要がなくなります。しかし、これを実装する前に、この二重ディスパッチが本当に必要かどうか、実行時ディスパッチが本当に必要かどうかを確認するために、設計について十分に検討する必要があります。
RTTI なしでコンパイルされているため、 LLVM がどのように実装されているかisa<>
、dyn_cast<>
およびテンプレート システムとして確認することをお勧めします。cast<>
少し面倒ですが (関連するすべてのクラスにちょっとしたコードが必要です)、非常に軽量です。
LLVM Programmer's Manualには、良い例と実装への参照があります。
(3 つのメソッドはすべて同じコードを共有しています)