ダブルディスパッチ

ダブルディスパッチは、多重ディスパッチのひとつの形態で、2個のオブジェクトから、それに対応する実際の手続きが実行時に決まる、というものである。近年のオブジェクト指向プログラミング言語でよく見られる obj.methodName というような構文では、obj に対応する1個のオブジェクトから、実行されるメソッドが決定される「シングルディスパッチ」であるわけだが、それに対して複数個のオブジェクトが関与して、多重定義されたメソッドなどから、実行される一つが決定されるのが多重ディスパッチで、多重ディスパッチに関与するオブジェクトを2個に限定したものがダブルディスパッチである。また、シングルディスパッチの言語における複数のクラス間で同様のことを実現するイディオムを指して言う場合もある。

1. 例
----

たとえば、以下のような状況でダブルディスパッチを活用することができる。

//二項演算子// ベクトル×行列、スカラ×ベクトル、など、ダブルディスパッチを活用する余地は大きい。
//適応的衝突判定アルゴリズム// では、通例物体により異なる方法で衝突を判定する必要がある。典型的な例では、ゲーム開発環境で、宇宙船と小惑星の衝突と、宇宙船と宇宙ステーションの衝突とは異なる方法で計算される。
//塗りつぶしアルゴリズム// 重なる可能性のある 2次元スプライトの描画の際には、スプライトの重なり部分を異なった方法で描画する必要がある。
//人事管理// システムでは、様々な種類の仕事を様々な種類の作業者に割り当てる。たとえば、経理担当者の型を持つオブジェクトが技術の型を持つ仕事に割り当てられた場合、schedule アルゴリズムは割り当てを拒絶する。
//イベント処理// では、イベントの型とイベントを受け付けるオブジェトの種類に応じて適切な処理ルーチンを呼び出す必要がある。

2. コスト
------

一般にメソッドディスパッチとは、引数の動的な型に応じて適切な手続きを選択して呼び出すことであり、オブジェクト指向言語の実行時におけるオーバヘッドとして重要な位置を占める。シングルディスパッチで、さらに多重継承などが無ければ、テーブルのオフセットをコンパイル時に静的に決定することなどもできるが、ダブルディスパッチでは組み合わせの数も多く、動的なディスパッチが必要になるなど、シングルディスパッチに比べコストは大きい。

3. 代替手法
-------

前述のように二項演算子という、多くのプログラミング言語で好まれている機能において望まれるものであるため、シングルディスパッチのみがあるオブジェクト指向プログラミング言語でダブルディスパッチのようなふるまいを実現する手法が考えられている。ここでは一例としてRubyのものを示す。

たとえばRubyに複素数クラスを自作して追加したいとする。Rubyでは二項演算子 + なども、左辺にあるオブジェクトに対するメソッド呼び出しなので、次のようなソースコードへの対応は自然に実装できる。 z1 = Complex.new z2 = z1 + 2.0 これに対し、次のようにも書きたいわけだが、 z3 = Complex.new z4 = 3.0 + z3 もし何も仕掛けが無ければ、あらゆる既存の数値クラスについて、「複素数を引数にした場合」を追加する必要があり現実的ではない。しかし、Rubyにおける数値関係のクラスの、演算子に対応するメソッドは次のようにふるまうようになっていて、 class Num def + if otherは既知のオブジェクト then return 結果 # 結果を計算して返す else left, right = other.coerce return left + right # coerceの結果により計算する end end end 追加したいクラスに coerce というメソッドを一つ定義し、適切な値を返すようにすれば、任意の演算子に対して望んだような結果にできる。

4. ダブルディスパッチは関数のオーバーロード以上である
----------------------------

一見したところでは、ダブルディスパッチは関数のオーバーロードの自然な結果である。関数のオーバーロードは呼び出されるクラスだけではなく、引数の型にも応じて呼び出しが行われるようにすることができるが、オーバーロードされた関数の呼び出しはほぼ一つの仮想関数テーブルを通じて行われるため、動的なディスパッチは呼び出すオブジェクトの種類によってのみ決まる。下記の例において、あるゲームで衝突の判定を行う場合を考える。 class SpaceShip {}; class GiantSpaceShip : public SpaceShip {};

class Asteroid { public: virtual void CollideWith { cout << "Asteroid hit a SpaceShip" << endl; } virtual void CollideWith { cout << "Asteroid hit a GiantSpaceShip" << endl; } };

class ExplodingAsteroid : public Asteroid { public: virtual void CollideWith { cout << "ExplodingAsteroid hit a SpaceShip" << endl; } virtual void CollideWith { cout << "ExplodingAsteroid hit a GiantSpaceShip" << endl; } };<WtXmlEndTag />

ここで、 Asteroid theAsteroid; SpaceShip theSpaceShip; GiantSpaceShip theGiantSpaceShip; があるとすると、関数のオーバーロードのために theAsteroid.CollideWith; theAsteroid.CollideWith;

上記のコードは、動的なディスパッチを使用せず、Asteroid hit a SpaceShip およびAsteroid hit a GiantSpaceShip とそれぞれ表示する。

さらに、 ExplodingAsteroid theExplodingAsteroid; theExplodingAsteroid.CollideWith; theExplodingAsteroid.CollideWith; 上記のコードはExplodingAsteroid hit a SpaceShip およびExplodingAsteroid hit a GiantSpaceShip と、やはり動的なディスパッチを使用せずに表示する。

Asteroidに対する参照を使って動的なディパッチを用いると、 Asteroid& theAsteroidReference = theExplodingAsteroid; theAsteroidReference.CollideWith; theAsteroidReference.CollideWith;

ExplodingAsteroid hit a SpaceShip およびExplodingAsteroid hit a GiantSpaceShipと期待通りに表示する。

しかし、 SpaceShip& theSpaceShipReference = theGiantSpaceShip; theAsteroid.CollideWith; theAsteroidReference.CollideWith; は、Asteroid hit a SpaceShip および ExplodingAsteroid hit a SpaceShipと表示するが、これはいずれも正しくない。問題は、仮想関数が C++ によって動的にディスパッチが行われるのに対して、関数のオーバーロードは静的に行われるためである。

4.1. C++ におけるダブルディスパッチ
----------------------

上述の問題は、Visitor パターンで用いられているものと同様の手法で解決できる。SpaceShip と GiantSpaceShip がいずれも関数 virtual void CollideWith { inAsteroid.CollideWith; } を持っているとすると、先ほどの例ではうまく動作しなかったが、以下の例はうまく動作する。 SpaceShip& theSpaceShipReference = theGiantSpaceShip; Asteroid& theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.CollideWith; theSpaceShipReference.CollideWith;

この例は、期待通りにAsteroid hit a GiantSpaceShip およびExplodingAsteroid hit a GiantSpaceShipと表示する。 鍵はtheSpaceShipReference.CollideWith; であり、 これはランタイムに下記のような動作をする。

theSpaceShipReference は参照であり、C++ は vtable から正しいメソッドを探し出し、GiantSpaceShip::CollideWithを呼び出す。
GiantSpaceShip::CollideWith 内では、inAsteroid は参照であるため、inAsteroid.CollideWith はもう一つの仮想関数テーブルの検索を行うことになる。この場合には、inAsteroid は ExplodingAsteroid への参照であり、ExplodingAsteroid::CollideWith が呼び出される。

5. 注
----

6. 関連項目
-------

Visitor パターン
多重ディスパッチ
仮想関数テーブル