「ダブルディスパッチ」の版間の差分
17行目: | 17行目: | ||
たとえばRubyに[[複素数]]クラスを自作して追加したいとする<ref>現在のcrubyには複素数クラスも組込みで存在するので、それが気になるなら[[四元数]]クラスなどなんでもよい。</ref>。Rubyでは二項演算子 <code>+</code> なども、左辺にあるオブジェクトに対するメソッド呼び出しなので、次のようなソースコードへの対応は自然に実装できる。 |
たとえばRubyに[[複素数]]クラスを自作して追加したいとする<ref>現在のcrubyには複素数クラスも組込みで存在するので、それが気になるなら[[四元数]]クラスなどなんでもよい。</ref>。Rubyでは二項演算子 <code>+</code> なども、左辺にあるオブジェクトに対するメソッド呼び出しなので、次のようなソースコードへの対応は自然に実装できる。 |
||
< |
<syntaxhighlight lang="ruby"> |
||
z1 = Complex.new(1.0, 0.0) |
z1 = Complex.new(1.0, 0.0) |
||
z2 = z1 + 2.0 |
z2 = z1 + 2.0 |
||
</syntaxhighlight> |
|||
</source> |
|||
これに対し、次のようにも書きたいわけだが、 |
これに対し、次のようにも書きたいわけだが、 |
||
< |
<syntaxhighlight lang="ruby"> |
||
z3 = Complex.new(0.0, 1.0) |
z3 = Complex.new(0.0, 1.0) |
||
z4 = 3.0 + z3 |
z4 = 3.0 + z3 |
||
</syntaxhighlight> |
|||
</source> |
|||
もし何も仕掛けが無ければ、あらゆる既存の数値クラスについて、「複素数を引数にした場合」を追加する必要があり現実的ではない。しかし、Rubyにおける数値関係のクラスの、演算子に対応するメソッドは次のようにふるまうようになっていて、 |
もし何も仕掛けが無ければ、あらゆる既存の数値クラスについて、「複素数を引数にした場合」を追加する必要があり現実的ではない。しかし、Rubyにおける数値関係のクラスの、演算子に対応するメソッドは次のようにふるまうようになっていて、 |
||
< |
<syntaxhighlight lang="ruby"> |
||
class Num |
class Num |
||
def +(other) |
def +(other) |
||
38行目: | 38行目: | ||
end |
end |
||
end |
end |
||
</syntaxhighlight> |
|||
</source> |
|||
追加したいクラス(たとえばここでは複素数クラス)に <code>coerce</code> というメソッドを一つ定義し、適切な値を返すようにすれば、任意の演算子に対して望んだような結果にできる。 |
追加したいクラス(たとえばここでは複素数クラス)に <code>coerce</code> というメソッドを一つ定義し、適切な値を返すようにすれば、任意の演算子に対して望んだような結果にできる。 |
||
45行目: | 45行目: | ||
一見したところでは、ダブルディスパッチは[[ポリモーフィズム|関数のオーバーロード]]の自然な結果である。関数のオーバーロードは呼び出されるクラスだけではなく、引数の型にも応じて呼び出しが行われるようにすることができるが、オーバーロードされた関数の呼び出しはほぼ一つの仮想関数テーブルを通じて行われるため、動的なディスパッチは呼び出すオブジェクトの種類によってのみ決まる。下記の例において、あるゲームで衝突の判定を行う場合を考える。 |
一見したところでは、ダブルディスパッチは[[ポリモーフィズム|関数のオーバーロード]]の自然な結果である。関数のオーバーロードは呼び出されるクラスだけではなく、引数の型にも応じて呼び出しが行われるようにすることができるが、オーバーロードされた関数の呼び出しはほぼ一つの仮想関数テーブルを通じて行われるため、動的なディスパッチは呼び出すオブジェクトの種類によってのみ決まる。下記の例において、あるゲームで衝突の判定を行う場合を考える。 |
||
< |
<syntaxhighlight lang="cpp"> |
||
class SpaceShip {}; |
class SpaceShip {}; |
||
class GiantSpaceShip : public SpaceShip {}; |
class GiantSpaceShip : public SpaceShip {}; |
||
68行目: | 68行目: | ||
} |
} |
||
}; |
}; |
||
</syntaxhighlight> |
|||
</source> |
|||
ここで、 |
ここで、 |
||
< |
<syntaxhighlight lang="cpp"> |
||
Asteroid theAsteroid; |
Asteroid theAsteroid; |
||
SpaceShip theSpaceShip; |
SpaceShip theSpaceShip; |
||
GiantSpaceShip theGiantSpaceShip; |
GiantSpaceShip theGiantSpaceShip; |
||
</syntaxhighlight> |
|||
</source> |
|||
があるとすると、関数のオーバーロードのために |
があるとすると、関数のオーバーロードのために |
||
< |
<syntaxhighlight lang="cpp"> |
||
theAsteroid.CollideWith(theSpaceShip); |
theAsteroid.CollideWith(theSpaceShip); |
||
theAsteroid.CollideWith(theGiantSpaceShip); |
theAsteroid.CollideWith(theGiantSpaceShip); |
||
</syntaxhighlight> |
|||
</source> |
|||
上記のコードは、動的なディスパッチを使用せず、 |
上記のコードは、動的なディスパッチを使用せず、 |
||
86行目: | 86行目: | ||
<code>Asteroid hit a GiantSpaceShip</code> とそれぞれ表示する。 |
<code>Asteroid hit a GiantSpaceShip</code> とそれぞれ表示する。 |
||
さらに、< |
さらに、<syntaxhighlight lang="cpp"> |
||
ExplodingAsteroid theExplodingAsteroid; |
ExplodingAsteroid theExplodingAsteroid; |
||
theExplodingAsteroid.CollideWith(theSpaceShip); |
theExplodingAsteroid.CollideWith(theSpaceShip); |
||
theExplodingAsteroid.CollideWith(theGiantSpaceShip); |
theExplodingAsteroid.CollideWith(theGiantSpaceShip); |
||
</ |
</syntaxhighlight> |
||
上記のコードは<code>ExplodingAsteroid hit a SpaceShip</code> および<code>ExplodingAsteroid hit a GiantSpaceShip</code> と、やはり動的なディスパッチを使用せずに表示する。 |
上記のコードは<code>ExplodingAsteroid hit a SpaceShip</code> および<code>ExplodingAsteroid hit a GiantSpaceShip</code> と、やはり動的なディスパッチを使用せずに表示する。 |
||
<code>Asteroid</code>に対する参照を使って動的なディパッチを用いると、 |
<code>Asteroid</code>に対する参照を使って動的なディパッチを用いると、 |
||
< |
<syntaxhighlight lang="cpp"> |
||
Asteroid& theAsteroidReference = theExplodingAsteroid; |
Asteroid& theAsteroidReference = theExplodingAsteroid; |
||
theAsteroidReference.CollideWith(theSpaceShip); |
theAsteroidReference.CollideWith(theSpaceShip); |
||
theAsteroidReference.CollideWith(theGiantSpaceShip); |
theAsteroidReference.CollideWith(theGiantSpaceShip); |
||
</syntaxhighlight> |
|||
</source> |
|||
<code>ExplodingAsteroid hit a SpaceShip</code> および |
<code>ExplodingAsteroid hit a SpaceShip</code> および |
||
104行目: | 104行目: | ||
しかし、 |
しかし、 |
||
< |
<syntaxhighlight lang="cpp"> |
||
SpaceShip& theSpaceShipReference = theGiantSpaceShip; |
SpaceShip& theSpaceShipReference = theGiantSpaceShip; |
||
theAsteroid.CollideWith(theSpaceShipReference); |
theAsteroid.CollideWith(theSpaceShipReference); |
||
theAsteroidReference.CollideWith(theSpaceShipReference); |
theAsteroidReference.CollideWith(theSpaceShipReference); |
||
</syntaxhighlight> |
|||
</source> |
|||
は、<code>Asteroid hit a SpaceShip</code> および <code>ExplodingAsteroid hit a SpaceShip</code>と表示するが、これはいずれも正しくない。問題は、仮想関数が C++ によって動的にディスパッチが行われるのに対して、関数のオーバーロードは静的に行われるためである。 |
は、<code>Asteroid hit a SpaceShip</code> および <code>ExplodingAsteroid hit a SpaceShip</code>と表示するが、これはいずれも正しくない。問題は、仮想関数が C++ によって動的にディスパッチが行われるのに対して、関数のオーバーロードは静的に行われるためである。 |
||
===C++ におけるダブルディスパッチ=== |
===C++ におけるダブルディスパッチ=== |
||
上述の問題は、[[Visitor パターン]]で用いられているものと同様の手法で解決できる。<code>SpaceShip</code> と <code>GiantSpaceShip</code> がいずれも関数 |
上述の問題は、[[Visitor パターン]]で用いられているものと同様の手法で解決できる。<code>SpaceShip</code> と <code>GiantSpaceShip</code> がいずれも関数 |
||
< |
<syntaxhighlight lang="cpp"> |
||
virtual void CollideWith(Asteroid& inAsteroid) { |
virtual void CollideWith(Asteroid& inAsteroid) { |
||
inAsteroid.CollideWith(*this); |
inAsteroid.CollideWith(*this); |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
を持っているとすると、先ほどの例ではうまく動作しなかったが、以下の例はうまく動作する。 |
を持っているとすると、先ほどの例ではうまく動作しなかったが、以下の例はうまく動作する。 |
||
< |
<syntaxhighlight lang="cpp"> |
||
SpaceShip& theSpaceShipReference = theGiantSpaceShip; |
SpaceShip& theSpaceShipReference = theGiantSpaceShip; |
||
Asteroid& theAsteroidReference = theExplodingAsteroid; |
Asteroid& theAsteroidReference = theExplodingAsteroid; |
||
theSpaceShipReference.CollideWith(theAsteroid); |
theSpaceShipReference.CollideWith(theAsteroid); |
||
theSpaceShipReference.CollideWith(theAsteroidReference); |
theSpaceShipReference.CollideWith(theAsteroidReference); |
||
</syntaxhighlight> |
|||
</source> |
|||
この例は、期待通りに |
この例は、期待通りに |
2020年7月5日 (日) 22:57時点における版
ダブルディスパッチ(英: double dispatch)は、多重ディスパッチのひとつの形態で、2個のオブジェクトから、それに対応する実際の手続きが決まる、というものである。近年のオブジェクト指向プログラミング言語でよく見られる obj.methodName(arg, ...) というような構文では、obj に対応する1個のオブジェクトから、実行されるメソッドが決定される「シングルディスパッチ」であるわけだが、それに対して複数個のオブジェクトが関与して、多重定義されたメソッドなどから、実行される一つが決定されるのが多重ディスパッチで、多重ディスパッチに関与するオブジェクトを2個に限定したものがダブルディスパッチである。また、シングルディスパッチの言語における複数のクラス間で同様のことを実現するイディオムを指して言う場合もある。[1]
例
たとえば、以下のような状況でダブルディスパッチを活用することができる。
- 二項演算子 ベクトル×行列、スカラ×ベクトル、など、ダブルディスパッチを活用する余地は大きい。
- 適応的衝突判定アルゴリズム では、通例物体により異なる方法で衝突を判定する必要がある。典型的な例では、ゲーム開発環境で、宇宙船と小惑星の衝突と、宇宙船と宇宙ステーションの衝突とは異なる方法で計算される。
- 塗りつぶしアルゴリズム 重なる可能性のある 2次元スプライトの描画の際には、スプライトの重なり部分を異なった方法で描画する必要がある。
- 人事管理 システムでは、様々な種類の仕事を様々な種類の作業者に割り当てる。たとえば、経理担当者の型を持つオブジェクトが技術の型を持つ仕事に割り当てられた場合、
schedule
アルゴリズムは割り当てを拒絶する。 - イベント処理 では、イベントの型とイベントを受け付けるオブジェトの種類に応じて適切な処理ルーチンを呼び出す必要がある。
コスト
一般にメソッドディスパッチとは、引数の動的な型に応じて適切な手続きを選択して呼び出すことであり、オブジェクト指向言語の実行時におけるオーバヘッドとして重要な位置を占める。シングルディスパッチで、さらに多重継承などが無ければ、テーブルのオフセットをコンパイル時に静的に決定することなどもできるが、ダブルディスパッチでは組み合わせの数も多く、動的なディスパッチが必要になるなど、シングルディスパッチに比べコストは大きい。
代替手法
前述のように二項演算子という、(LispやForthなどを除いた)多くのプログラミング言語で好まれている機能において望まれるものであるため、シングルディスパッチのみがあるオブジェクト指向プログラミング言語でダブルディスパッチのようなふるまいを実現する手法が考えられている。ここでは一例として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
end
end
追加したいクラス(たとえばここでは複素数クラス)に coerce
というメソッドを一つ定義し、適切な値を返すようにすれば、任意の演算子に対して望んだような結果にできる。
ダブルディスパッチは関数のオーバーロード以上である
一見したところでは、ダブルディスパッチは関数のオーバーロードの自然な結果である。関数のオーバーロードは呼び出されるクラスだけではなく、引数の型にも応じて呼び出しが行われるようにすることができるが、オーバーロードされた関数の呼び出しはほぼ一つの仮想関数テーブルを通じて行われるため、動的なディスパッチは呼び出すオブジェクトの種類によってのみ決まる。下記の例において、あるゲームで衝突の判定を行う場合を考える。
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&)
が呼び出される。
注
- ^ http://www.infoq.com/jp/articles/DoubleDispatch_0829
- ^ 現在のcrubyには複素数クラスも組込みで存在するので、それが気になるなら四元数クラスなどなんでもよい。