ダブルディスパッチ

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

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

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

コスト

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

代替手法

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

たとえばRubyに複素数クラスを自作して追加したいとする[2]。Rubyでは二項演算子 + なども、左辺にあるオブジェクトに対するメソッド呼び出しなので、次のようなソースコードへの対応は自然に実装できる。

z1 = Complex.new(1.0, 0.0)z2 = z1 + 2.0

これに対し、次のようにも書きたいわけだが、

z3 = Complex.new(0.0, 1.0)z4 = 3.0 + z3

もし何も仕掛けが無ければ、あらゆる既存の数値クラスについて、「複素数を引数にした場合」を追加する必要があり現実的ではない。しかし、Rubyにおける数値関係のクラスの、演算子に対応するメソッドは次のようにふるまうようになっていて、

class Num  def +(other)    if otherは既知のオブジェクト then      return 結果  # 結果を計算して返す    else      left, right = other.coerce(self)      return left + right  # coerceの結果により計算する    end  endend

追加したいクラス(たとえばここでは複素数クラス)に coerce というメソッドを一つ定義し、適切な値を返すようにすれば、任意の演算子に対して望んだような結果にできる。

ダブルディスパッチはメソッドのオーバーロード以上である

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

// SpaceShip側class SpaceShip {};class GiantSpaceShip : public SpaceShip {};// Asteroid側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;

があるとすると、下記のように処理できる。

// Asteroid側、SpaceShip側 共に静的theAsteroid.CollideWith(theSpaceShip);theAsteroid.CollideWith(theGiantSpaceShip);

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

さらに、

// Asteroid側、SpaceShip側 共に静的ExplodingAsteroid theExplodingAsteroid;theExplodingAsteroid.CollideWith(theSpaceShip);theExplodingAsteroid.CollideWith(theGiantSpaceShip);

上記のコードはExplodingAsteroid hit a SpaceShip およびExplodingAsteroid hit a GiantSpaceShip と、やはり動的なディスパッチを使わず表示する。

がここでtheExplodingAsteroidを、Asteroidに対する参照(かポインタ)を経由すると、

// Asteroid側は動的、SpaceShip側は静的Asteroid& theAsteroidReference = theExplodingAsteroid;theAsteroidReference.CollideWith(theSpaceShip);theAsteroidReference.CollideWith(theGiantSpaceShip);

動的な(シングル)ディスパッチが起きた結果 Asteroid のメソッドでなく、ExplodingAsteroid のメソッドが呼ばれ、

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

しかし

SpaceShip& theSpaceShipReference = theGiantSpaceShip;theAsteroid.CollideWith(theSpaceShipReference);          // Asteroid側は静的、SpaceShip側は動的theAsteroidReference.CollideWith(theSpaceShipReference); // Asteroid側は動的、SpaceShip側は動的

という処理では、Asteroid hit a SpaceShip および ExplodingAsteroid hit a SpaceShipと表示されてしまい、本来表示されるべき GiantSpaceShip の文言が表示されない。

原因は、Asteroid側の動的なシングルディスパッチしか実現できておらず、SpaceShip側も含めた動的なダブルディスパッチができていない事である。

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

上述の問題は、Visitor パターンで用いられているものと同様の手法で解決できる。SpaceShipGiantSpaceShip がいずれも関数

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); であり、これは下記のような2回のディスパッチをする。

  1. theSpaceShipReference は参照であり、C++ は theSpaceShipReference.vtable から正しいメソッドを探し出し、GiantSpaceShip::CollideWith(Asteroid&)を呼び出す(1つ目のディスパッチ)。
  2. GiantSpaceShip::CollideWith(Asteroid&) 内では、inAsteroid は参照であるため、inAsteroid.CollideWith(*this) は inAsteroid.vtable の検索を行う。この場合、inAsteroidExplodingAsteroid への参照で、ExplodingAsteroid::CollideWith(GiantSpaceShip&) が呼ばれる(2つ目のディスパッチ)。

関連項目