ダブルディスパッチ
ダブルディスパッチ(英: double dispatch)とは、ソフトウェア工学において、関数呼び出しの際に関連した複数のオブジェクトの実行時の型に応じて異なる具体的な関数を割り当てる機構である。同様の概念として多重ディスパッチがある。大半のオブジェクト指向システムでは、ある関数呼び出しから実際に呼び出される関数は、単一のオブジェクトの動的な型にのみ依存し、したがってシングルディスパッチの呼び出し、あるいは仮想関数の呼び出しと呼ばれる。
目次 |
例 [編集]
ダブルディスパッチは計算結果が引数の動的な型に依存する場合に有用である。たとえば、以下のような状況でダブルディスパッチを活用することができる。
- ""適応的衝突判定アルゴリズム"" では、通例物体により異なる方法で衝突を判定する必要がある。典型的な例では、ゲーム開発環境で、宇宙船と小惑星の衝突と、宇宙船と宇宙ステーションの衝突とは異なる方法で計算される。
- 塗りつぶしアルゴリズム 重なる可能性のある 2次元スプライトの描画の際には、スプライトの重なり部分を異なった方法で描画する必要がある。
- 人事管理 システムでは、様々な種類の仕事を様々な種類の作業者に割り当てる。たとえば、経理担当者の型を持つオブジェクトが技術の型を持つ仕事に割り当てられた場合、
scheduleアルゴリズムは割り当てを拒絶する。 - イベント処理 では、イベントの型とイベントを受け付けるオブジェトの種類に応じて適切な処理ルーチンを呼び出す必要がある。
共通のイディオム [編集]
上記の例に現れる共通のイディオムは、呼び出しの引数の動的な型に応じた適切なアルゴリズムの選択である。したがって、動的に呼び出しを解決するために必要なパフォーマンス上のトレードオフが生じ、その影響はシングルディスパッチをサポートする言語よりも通常大きい。C++における動的な関数の呼び出しは、コンパイラがオブジェクトのメソッドテーブル(vtable)内の関数の位置を知っているため、静的にオフセットの演算を行うことで解決できる。ダブルディスパッチをサポートする言語では、メソッドのテーブル内のオフセットを動的に計算しなければならないため、よりコストが大きい。
ダブルディスパッチは関数のオーバーロード以上である [編集]
一見したところでは、ダブルディスパッチは関数のオーバーロードの自然な結果である。関数のオーバーロードは呼び出されるクラスだけではなく、引数の型にも応じて呼び出しが行われるようにすることができるが、オーバーロードされた関数の呼び出しはほぼ一つの仮想関数テーブルを通じて行われるため、動的なディスパッチは呼び出すオブジェクトの種類によってのみ決まる。下記の例において、あるゲームで衝突の判定を行う場合を考える。
class SpaceShip {}; class GiantSpaceShip : public SpaceShip {}; class Asteroid { public: virtual void CollideWith(SpaceShip&) { cout << "Asteroid hit a SpaceShip" << endl; } virtual void CollideWith(GiantSpaceShip&) { cout << "Asteroid hit a GiantSpaceShip" << endl; } }; class ExplodingAsteroid : public Asteroid { public: virtual void CollideWith(SpaceShip&) { cout << "ExplodingAsteroid hit a SpaceShip" << endl; } virtual void CollideWith(GiantSpaceShip&) { cout << "ExplodingAsteroid hit a GiantSpaceShip" << endl; } };
ここで、
Asteroid theAsteroid; SpaceShip theSpaceShip; GiantSpaceShip theGiantSpaceShip;
があるとすると、関数のオーバーロードのために
theAsteroid.CollideWith(theSpaceShip); theAsteroid.CollideWith(theGiantSpaceShip);
上記のコードは、動的なディスパッチを使用せず、 Asteroid hit a SpaceShip および Asteroid hit a GiantSpaceShip とそれぞれ表示する。
さらに、
ExplodingAsteroid theExplodingAsteroid; theExplodingAsteroid.CollideWith(theSpaceShip); theExplodingAsteroid.CollideWith(theGiantSpaceShip);
上記のコードはExplodingAsteroid hit a SpaceShip およびExplodingAsteroid hit a GiantSpaceShip と、やはり動的なディスパッチを使用せずに表示する。
Asteroidに対する参照を使って動的なディパッチを用いると、
Asteroid& theAsteroidReference = theExplodingAsteroid; theAsteroidReference.CollideWith(theSpaceShip); theAsteroidReference.CollideWith(theGiantSpaceShip);
ExplodingAsteroid hit a SpaceShip および ExplodingAsteroid hit a GiantSpaceShipと期待通りに表示する。
しかし、
SpaceShip& theSpaceShipReference = theGiantSpaceShip; theAsteroid.CollideWith(theSpaceShipReference); theAsteroidReference.CollideWith(theSpaceShipReference);
は、Asteroid hit a SpaceShip および ExplodingAsteroid hit a SpaceShipと表示するが、これはいずれも正しくない。問題は、仮想関数が C++ によって動的にディスパッチが行われるのに対して、関数のオーバーロードは静的に行われるためである。
C++ におけるダブルディスパッチ [編集]
上述の問題は、Visitor パターンで用いられているものと同様の手法で解決できる。SpaceShip と GiantSpaceShip がいずれも関数
virtual void CollideWith(Asteroid& inAsteroid) { inAsteroid.CollideWith(*this); }
を持っているとすると、先ほどの例ではうまく動作しなかったが、以下の例はうまく動作する。
SpaceShip& theSpaceShipReference = theGiantSpaceShip; Asteroid& theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.CollideWith(theAsteroid); theSpaceShipReference.CollideWith(theAsteroidReference);
この例は、期待通りに Asteroid hit a GiantSpaceShip および ExplodingAsteroid hit a GiantSpaceShipと表示する。 鍵はtheSpaceShipReference.CollideWith(theAsteroidReference); であり、 これはランタイムに下記のような動作をする。
theSpaceShipReferenceは参照であり、C++ は vtable から正しいメソッドを探し出し、GiantSpaceShip::CollideWith(Asteroid&)を呼び出す。GiantSpaceShip::CollideWith(Asteroid&)内では、inAsteroidは参照であるため、inAsteroid.CollideWith(*this)はもう一つの仮想関数テーブルの検索を行うことになる。この場合には、inAsteroidはExplodingAsteroidへの参照であり、ExplodingAsteroid::CollideWith(GiantSpaceShip&)が呼び出される。