多重ディスパッチまたはマルチメソッドは、一部のプログラミング言語の機能であり、関数またはメソッドを、実行時(動的)型、またはより一般的な場合には、その引数の複数の属性に基づいて動的にディスパッチすることができます。[ 1 ]これは、関数またはメソッド呼び出しが、メソッドが呼び出されたオブジェクトの派生型に基づいて動的にディスパッチされる単一ディスパッチ多態性の一般化です。多重ディスパッチは、1 つ以上の引数の結合された特性を使用して、動的ディスパッチを実装関数またはメソッドにルーティングします。
コンピュータソフトウェアの開発者は通常、ソースコードをサブルーチン、プロシージャ、サブプログラム、関数、メソッドなどと呼ばれる名前付きブロックに整理します。関数内のコードは、関数を呼び出すことによって実行されます。つまり、関数名を参照するコードを実行するのです。これにより、一時的に制御が呼び出された関数に移ります。関数の実行が完了すると、通常は参照先の呼び出し元の命令に制御が戻されます。
関数名は通常、その関数の目的を的確に表すように選ばれます。しかし、概念的に類似したタスクを実行するものの、異なる種類の入力データを扱う場合など、複数の関数に同じ名前を付けることが望ましい場合もあります。このような場合、関数呼び出し箇所での名前参照だけでは、実行するコードブロックを特定するには不十分です。そのため、関数呼び出しの引数の数と型も、複数の関数実装の中から適切なものを選択するために使用されます。
より一般的な、つまり単一ディスパッチのオブジェクト指向プログラミング言語では、メソッドを呼び出すとき(Smalltalkでメッセージを送信したり、C++でメンバ関数を呼び出したりする場合)、その引数の 1 つが特別に扱われ、その名前のメソッドのクラス(場合によっては多数)のうちどれを適用するかを決定するために使用されます。多くの言語では、特別な引数は構文的に示されます。たとえば、多くのプログラミング言語では、メソッド呼び出しを行う際に特別な引数をドットの前に置きます。つまり、 は轟音を発し、 はさえずりを発します。special.method(other, arguments, here)lion.sound()sparrow.sound()
対照的に、多重ディスパッチ機能を持つ言語では、選択されるメソッドは、関数呼び出しの番号と型に一致する引数を持つメソッドのみです。特定の呼び出しで実行される関数/メソッドを所有する特別な引数は存在しません。
多重ディスパッチは、関数オーバーロードとは区別されるべきである。関数オーバーロードでは、項の宣言型や推論型(サブタイピングを持つ言語では基本型)などの静的型情報を使用して、特定の呼び出し箇所で複数の選択肢のうちどれを使用するかを決定する。この決定はコンパイル時またはリンク時(あるいはプログラム実行開始前の別の時点)に行われ、その後はプログラムの特定の展開または実行に対して不変となる。C++などの多くの言語は堅牢な関数オーバーロードを提供するが、動的多重ディスパッチは提供しない(C++は仮想関数の使用による動的単一ディスパッチのみを許可する)。
コンパイル時にデータ型を識別できる言語を使用する場合、複数の選択肢の中から最適なものを選ぶことができます。コンパイル時に選択を行うための代替関数を作成する行為は、通常、関数のオーバーロードと呼ばれます。
データ型の識別を実行時まで延期するプログラミング言語(つまり、遅延バインディング)では、関数引数の動的に決定される型に基づいて、代替関数の選択が行われます。このようにして代替実装が選択される関数は、一般的にマルチメソッドと呼ばれます。
関数呼び出しを動的にディスパッチするには、実行時コストが伴います。一部の言語では、オーバーロードとマルチメソッドの区別が曖昧になる場合があり、コンパイラが特定の関数呼び出しに対してコンパイル時選択を適用できるかどうか、あるいはより低速な実行時ディスパッチが必要かどうかを判断します。
動的ディスパッチには、単一ディスパッチと複数ディスパッチの両方において、いくつかの既知の問題があります。これらの問題の多くは、オブジェクト指向プログラミング言語の標準機能として数十年にわたって採用されてきた単一ディスパッチでは解決されていますが、複数ディスパッチの場合は、これらの問題がより複雑になります。
ほとんどの一般的なプログラミング言語では、ソースコードは機能の単位(ここではパッケージと呼びます)で提供および展開されます。この概念の実際の用語は言語によって異なります。各パッケージには複数の型、値、関数定義が含まれる場合があり、コンパイルステップのある言語ではパッケージは個別にコンパイルされることが多く、非循環的な依存関係が存在する場合があります。完全なプログラムはパッケージの集合であり、メインパッケージは他の複数のパッケージに依存する可能性があり、プログラム全体は依存関係の推移閉包で構成されます。
いわゆる式の問題とは、依存パッケージ内のコードが、包含パッケージ内から、ベースパッケージのソースコードを変更することなく、ベースパッケージで定義された動作(関数またはデータ型)を拡張できるかどうかという問題です。従来の単一ディスパッチのオブジェクト指向言語では、新しいデータ型を追加するのは簡単ですが、新しい関数を追加するのは簡単ではありません。従来の関数型言語ではその逆の効果が生じる傾向があり、多重ディスパッチは正しく実装されていれば、両方を可能にします。多重ディスパッチの実装には、以下の特性が望ましいです。
マルチメソッドの任意の呼び出しに対して、マルチメソッドの実装ケースの中で「最良」の候補が最大で 1 つだけ存在し、かつ、そうでない場合は、失敗を含めて予測可能かつ決定論的な方法で解決されることが一般的に望ましい。非決定論的な動作は望ましくない。非循環サブタイピング関係を持つ型のセットを仮定すると、マルチメソッドの実装のうち 1 つが「より優れている」(より具体的)とは、最初の実装で動的にディスパッチされるすべての引数が、2 番目の実装で指定されたすべての動的にディスパッチされる引数のサブタイプであり、かつ少なくとも 1 つが厳密なサブタイプである場合であると定義できる。単一ディスパッチで多重継承がない場合、この条件は自明に満たされるが、多重ディスパッチの場合、2 つ以上の候補が特定の実際の引数リストを満たす可能性があるが、どちらも他より具体的ではない(一方のケースでは 1 つの動的引数がサブタイプであり、もう 1 つのケースでは別の動的引数がサブタイプである)。これは特に、互いに依存していない2つの異なるパッケージが、それぞれ自身のパッケージの型に関する実装を持つマルチメソッドを拡張し、その後、両方を(間接的に)含む第3のパッケージが、両方のパッケージからの引数を使用してマルチメソッドを呼び出す場合に発生する可能性があります。
考えられる解決策は以下のとおりです。
オブジェクトコードに個別にコンパイルされ、低レベル(言語非対応)リンカでリンクされるプログラミング言語を含め、プログラムのロード/開始時に動的に、あるいはアプリケーションコードの指示の下でさえ、シングルディスパッチの効率的な実装はよく知られています。C++やその他の初期のオブジェクト指向言語で開発された「 vtable」メソッド(各クラスがそのクラスの仮想関数に対応する関数ポインタの配列を持つ)は、最適化されていない場合でもO(1)のオーバーヘッドと1回のメモリ検索しか必要とせず、静的メソッド呼び出しとほぼ同じ速度です。ただし、vtableメソッドは引数の型ではなく関数名を検索キーとして使用するため、マルチディスパッチの場合には拡張性がありません。(また、メソッドが特定のデータ型とは独立した独立したエンティティではなく、クラスの機能であるというオブジェクト指向パラダイムに依存しています。)
複数ディスパッチの効率的な実装は、依然として継続的な研究課題である。
実際に多重ディスパッチがどのくらいの頻度で使用されているかを推定するために、Muschevici ら[ 2 ]は動的ディスパッチを使用するプログラムを調査しました。彼らは、Common Lisp Object System、Dylan、Cecil、MultiJava、Diesel、Nice の 6 つの異なる言語で書かれた、主にコンパイラである 9 つのアプリケーションを分析しました。彼らの結果によると、汎用関数の 13 ~ 32% は 1 つの引数の動的型を使用し、2.7 ~ 6.5% は複数の引数の動的型を使用しています。残りの 65 ~ 93% の汎用関数は 1 つの具体的なメソッド (オーバーライド) を持っているため、引数の動的型を使用しているとはみなされません。さらに、この研究では、汎用関数の 2 ~ 20% が 2 つの具体的な関数実装を持ち、3 ~ 6% が 3 つの具体的な関数実装を持っていると報告しています。具体的なオーバーライドが多い関数では、その数は急速に減少します。
多重ディスパッチはJuliaでより多用されており、多重ディスパッチは言語の起源から中心的な設計概念でした。Muschevici と同じ統計を汎用関数あたりの平均メソッド数について収集したところ、Julia標準ライブラリは Muschevici が分析した他の言語の 2 倍以上のオーバーロードを使用しており、二項演算子の場合は 10 倍以上であることがわかりました。[ 3 ]
これらの論文からのデータは、次の表にまとめられています。ここで、ディスパッチ比率DRは汎用関数あたりのメソッドの平均数、選択比率CRはメソッド数の二乗の平均(メソッド数が多い関数の頻度をより正確に測定するため)、[ 2 ] [ 3 ]、特殊化の度合いDoSはメソッドあたりの型特殊化された引数の平均数(つまり、ディスパッチされる引数の数)です。
多重ディスパッチ言語の理論は、Castagna らによって、遅延バインディングを持つオーバーロード関数のモデルを定義することによって初めて開発されました。[ 4 ] [ 5 ]これは、オブジェクト指向言語の型バリアンス(共変性と反変性)の問題の最初の形式化[ 6 ]と、バイナリメソッドの問題の解決策をもたらしました。[ 7 ]
多重ディスパッチと単一ディスパッチの違いは、例を挙げるとより分かりやすくなるでしょう。例えば、宇宙船や小惑星といった(ユーザーに見える)オブジェクトが登場するゲームを想像してみてください。2つのオブジェクトが衝突した場合、プログラムは衝突したオブジェクトに応じて異なる処理を行う必要があるかもしれません。
C#はバージョン 4 [ 8 ] (2010 年 4 月) で 'dynamic' キーワードを使用して動的マルチメソッドのサポートを導入しました。次の例はマルチメソッドを示しています。他の多くの静的型付け言語と同様に、C# も静的メソッドのオーバーロードをサポートしています。 [ 9 ] Microsoft は、ほとんどのシナリオで開発者が動的型付けよりも静的型付けを選択すると予想しています。[ 10 ] 'dynamic' キーワードは、COM オブジェクトおよび動的型付け .NET 言語との相互運用性をサポートします。
![]()
以下の例では、C# 9 および C# 10 で導入された機能を使用しています。
static ColliderLibraryを使用する;Console.WriteLine ( Collide ( new Asteroid ( 101 ), new Spaceship ( 300 ))); Console.WriteLine ( Collide ( new Asteroid ( 10 ) , new Spaceship ( 10 ) )); Console.WriteLine ( Collide ( new Spaceship ( 101 ) , new Spaceship ( 10 ) ) ) ;string Collide ( SpaceObject x , SpaceObject y ) => x.Size > 100 && y.Size > 100 ? "Big boom!" : CollideWith ( x as dynamic , y as dynamic ) ; // CollideWith メソッドへの動的ディスパッチclass ColliderLibrary { public static string CollideWith ( Asteroid x , Asteroid y ) => "a/a" ; public static string CollideWith ( Asteroid x , Spaceship y ) => "a/s" ; public static string CollideWith ( Spaceship x , Asteroid y ) => "s/a" ; public static string CollideWith ( Spaceship x , Spaceship y ) => "s/s" ; }abstract record SpaceObject ( int Size ); record Asteroid ( int Size ) : SpaceObject ( Size ); record Spaceship ( int Size ) : SpaceObject ( Size );出力:
大爆発!a/s s/sGroovyは汎用的なJava互換/相互利用可能なJVM言語であり、Javaとは異なり、遅延バインディング/多重ディスパッチを使用します。[ 11 ]
/* 上記の C# の例の Groovy 実装。 非静的メソッドを使用する場合や、クラス/メソッドを静的にコンパイルする場合 (@CompileStatic アノテーション)でも、遅延バインディングは同じように機能します。 */ class Program { static void main ( String [] args ) { println Collider . collide ( new Asteroid ( 101 ), new Spaceship ( 300 )) println Collider . collide ( new Asteroid ( 10 ), new Spaceship ( 10 )) println Collider . collide ( new Spaceship ( 101 ), new Spaceship ( 10 )) } }class Collider { static String collide ( SpaceObject x , SpaceObject y ) { ( x . size > 100 && y . size > 100 ) ? "big-boom" : collideWith ( x , y ) // collideWith メソッドへの動的ディスパッチ}private static String collideWith ( Asteroid x , Asteroid y ) { "a/a" } private static String collideWith ( Asteroid x , Spaceship y ) { "a/s" } private static String collideWith ( Spaceship x , Asteroid y ) { "s/a" } private static String collideWith ( Spaceship x , Spaceship y ) { "s/s" } }class SpaceObject { int size SpaceObject ( int size ) { this . size = size } }@InheritConstructors class Asteroid extends SpaceObject {} @InheritConstructors class Spaceship extends SpaceObject {}Common Lispのような多重ディスパッチ言語では、以下のような記述になるかもしれません(Common Lispの例を示します)。
( defclass asteroid () (( size :reader size :initarg :size ))) ( defclass spaceship () (( size :reader size :initarg :size ))) ( defun space-object ( class size ) ( make-instance class :size size )); collide-with は、複数のディスパッチを持つ汎用関数です( defmethod collide-with (( x asteroid ) ( y asteroid )) "a/a" ) ( defmethod collide-with (( x asteroid ) ( y spaceship )) "a/s" ) ( defmethod collide-with (( x spaceship ) ( y asteroid )) "s/a" ) ( defmethod collide-with (( x spaceship ) ( y spaceship )) "s/s" )( defun collide ( x y ) ( if ( and ( > ( size x ) 100 ) ( > ( size y ) 100 )) "big-boom" ( collide-with x y )))( print ( collide ( space-object 'asteroid 101 ) ( space-object 'spaceship 300 ))) ( print ( collide ( space-object 'asteroid 10 ) ( space-object 'spaceship 10 ))) ( print ( collide ( space-object 'spaceship 101 ) ( space-object 'spaceship 10 )))他の方法についても同様です。明示的なテストや「動的キャスト」は使用しません。
多重ディスパッチが存在する場合、メソッドがクラスで定義され、オブジェクトに含まれるという従来の考え方は魅力が薄れます。上記の各衝突メソッドは、1つのクラスではなく、2つの異なるクラスに関連付けられます。そのため、メソッド呼び出しのための特別な構文は一般的に消滅し、メソッド呼び出しは通常の関数呼び出しとまったく同じになり、メソッドはクラスではなく汎用関数にグループ化されます。
Juliaには多重ディスパッチ機能が組み込まれており、これは言語設計の中核をなすものです。[ 3 ] 上記の例のJulia版は次のようになります。
抽象型SpaceObject終了struct Asteroid <: SpaceObject size :: Int end struct Spaceship <: SpaceObject size :: Int endcollide_with ( :: Asteroid , :: Spaceship ) = "a/s" collide_with ( :: Spaceship , :: Asteroid ) = "s/a" collide_with ( :: Spaceship , :: Spaceship ) = "s/s" collide_with ( :: Asteroid , :: Asteroid ) = "a/a"collide ( x :: SpaceObject , y :: SpaceObject ) = ( x . size > 100 && y . size > 100 ) ? "Big boom!" : collide_with ( x , y )出力:
julia> collide ( Asteroid ( 101 ), Spaceship ( 300 )) "大きな爆発!"julia> collide ( Asteroid ( 10 ), Spaceship ( 10 )) "a/s"julia> collide ( Spaceship ( 101 ), Spaceship ( 10 )) "s/s"RakuはPerlと同様に、他の言語で実証済みのアイデアを採用しており、型システムはコンパイラ側のコード解析において説得力のある利点を提供し、多重ディスパッチによる強力なユーザー側のセマンティクスを実現することが証明されている。
マルチメソッドとマルチサブルーチンの両方を備えています。ほとんどの演算子はサブルーチンであるため、複数のディスパッチ演算子も備えています。
通常の型制約に加えて、非常に特殊なサブルーチンを作成できる場所制約も備えています。
実数のサブセットMassで0 ^..^ Inf です。 役割Stellar-Object { Mass $.massが必要です。 メソッド名()はStr {...}を返します。 } class Asteroid does Stellar-Object { method name () { 'an asteroid' } } class Spaceship does Stellar-Object { has Str $.name = 'some unnamed spaceship' ; } my Str @destroyed = < obliterated destroyed mangled >; my Str @damaged = « damage 'collided with' 'was damaged by' »; # 数値比較演算子に複数候補を追加します。これは、数値的に比較しているためですが、 # オブジェクトを数値型に強制変換しても意味がありません。# ( 強制変換する場合は、これらの演算子を追加する必要は必ずしもありません。 ) # まったく新しい演算子を同じように定義することもできます。multi sub infix: « <=> » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $ a . mass <=> $b . mass } multi sub infix: « < » ( Stellar-Object : D $ a , Stellar-Object: D $b ) { $a . mass < $b . mass } multi sub infix: « > » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass > $b .質量} 多重サブ中置: « == » (恒星オブジェクト:D $a、恒星オブジェクト:D $b ) { $a . mass == $b . mass }# 新しいマルチディスパッチャを定義し、パラメータにいくつかの型制約を追加します。# 定義しなかった場合、制約のない汎用的なものが取得されます。proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*} # プロトタイプと同じなので、ここで型を繰り返す必要はありません。# 'where' 制約は技術的には $b にのみ適用され、シグネチャ全体には適用されません。# 'where' 制約では、先ほど追加した `<` 演算子候補が使用されていることに注意してください。multi sub collide ( $a , $b where $a < $b ) { say "$a.name() は $b.name() によって @destroyed.pick() されました" ; } multi sub collide ( $a , $b where $a > $b ) { # 引数を交換して前の候補に再ディスパッチしますsamewith $b , $a ; } # これは最初の 2 つの後になければなりません。なぜなら、他のものには「where」制約があり、サブが書かれた順序でチェックされるからです。 (これは常に一致します。) multi sub collide ( $a , $b ) { # 順序をランダム化しますmy ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 @damaged.pick() $n2" ; } # 次の 2 つの候補は、プロトの後のどこにでも配置できます。# なぜなら、それらは前の 3 つよりも特殊化された型を持っているからです。# 船の質量が異なる場合は、最初の 2 つの候補のいずれかが代わりに呼び出されます。multi sub collide ( Spaceship $a , Spaceship $b where $a == $b ){ my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 が $n2 と衝突し、両方の船が "、 ( @destroyed . pick , 'left damaged' ). pick ; } # 属性をシグネチャ内の変数に展開できます。# `(:mass($a) where 10)` のように制約を設けることもできます。multi sub collide ( Asteroid $ (: mass ( $a )), Asteroid $ (: mass ( $b )) ){ say "2 つの小惑星が衝突し、質量 { $a + $b } の 1 つのより大きな小惑星に結合しました" ; } my Spaceship $Enterprise .= new (: mass ( 1 ),: name ( 'The Enterprise' )); collide Asteroid . new (: mass ( .1 )), $Enterprise ; collide $Enterprise , Spaceship . new (: mass ( .1 ) ); collide $ Enterprise , Asteroid . new ( : mass ( 1 ) ); collide $ Enterprise , Spaceship . new ( : mass ( 10 )), Asteroid . new ( : mass ( 5 ) ) ;言語定義や構文レベルで多重ディスパッチをサポートしていない言語では、ライブラリ拡張機能を使用して多重ディスパッチを追加できる場合が多い。JavaScript と TypeScript は構文レベルでマルチメソッドをサポートしていないが、ライブラリを介して多重ディスパッチを追加することは可能である。例えば、multimethod パッケージ[ 12 ]は、多重ディスパッチの汎用関数の実装を提供している。
JavaScriptにおける動的型付けバージョン:
import { multi , method } from '@arrows/multimethod'クラスAsteroid {}クラスSpaceship {}const collideWith = multi ( method ([ Asteroid , Asteroid ], ( x , y ) => { // 小惑星が小惑星に衝突する場合の処理 }), method ([ Asteroid , Spaceship ], ( x , y ) => { // 小惑星が宇宙船に衝突する場合の処理 }), method ([ Spaceship , Asteroid ], ( x , y ) => { // 宇宙船が小惑星に衝突する場合の処理 }), method ([ Spaceship , Spaceship ], ( x , y ) => { // 宇宙船が宇宙船に衝突する場合の処理 }), )TypeScriptによる静的型付けバージョン:
import { multi , method , Multi } from '@arrows/multimethod'クラスAsteroid {}クラスSpaceship {}type CollideWith = Multi & { ( x : Asteroid , y : Asteroid ) : void ( x : Asteroid , y : Spaceship ) : void ( x : Spaceship , y : Asteroid ) : void ( x : Spaceship , y : Spaceship ) : void }const collideWith : CollideWith = multi ( method ([ Asteroid , Asteroid ], ( x , y ) => { // 小惑星が小惑星に衝突する場合の処理 }), method ([ Asteroid , Spaceship ], ( x , y ) => { // 小惑星が宇宙船に衝突する場合の処理 }), method ([ Spaceship , Asteroid ], ( x , y ) => { // 宇宙船が小惑星に衝突する場合の処理 }), method ([ Spaceship , Spaceship ], ( x , y ) => { // 宇宙船が宇宙船に衝突する場合の処理 }), )ライブラリ拡張機能を使用することで、 Pythonに多重ディスパッチを追加できます。たとえば、multimethod.pyモジュール[ 13 ]や、言語の基本的な構文やキーワードを変更することなくCLOSスタイルのマルチメソッドをPythonに提供するmultimethods.pyモジュール[ 14 ]を使用できます。
from typing import Anyimport game_behaviors from game_objects import Asteroid , Spaceship from multimethods import Dispatchcollide : Dispatch = Dispatch () collide . add_rule (( Asteroid , Spaceship ), game_behaviors . as_func ) collide . add_rule (( Spaceship , Spaceship ), game_behaviors . ss_func ) collide . add_rule (( Spaceship , Asteroid ), game_behaviors . sa_func )def aa_func ( a : Any , b : Any ) -> None : """小惑星が小惑星に衝突したときの動作。""" # ...新しい動作を定義...collide.add_rule ( ( Asteroid , Asteroid ) , aa_func )# ...後で...衝突( thing1 、thing2 )機能的にはCLOSの例と非常によく似ていますが、構文は一般的なPythonです。
デコレータ(Python 2.4以降に導入)を使用して、 Guido van Rossumは簡略化された構文でマルチメソッドのサンプル実装を作成しました[ 15 ] 。
@multimethod ( Asteroid , Asteroid ) def collide ( a : Asteroid , b : Asteroid ) -> None : """小惑星が小惑星に衝突したときの動作。""" # ...新しい動作を定義...@multimethod ( Asteroid , Spaceship ) def collide ( a : Asteroid , b : Spaceship ) -> None : """小惑星が宇宙船に衝突したときの動作""" # ...新しい動作を定義...# ...その他のマルチメソッドルールを定義する...そして、マルチメソッドデコレータの定義へと続きます。
PEAK-Rules パッケージは、上記の例と同様の構文で多重ディスパッチを提供します。[ 16 ]これは後に PyProtocols に置き換えられました。[ 17 ]
Regライブラリは、多重ディスパッチと述語ディスパッチもサポートしています。[ 18 ]
型ヒント の導入により、多重ディスパッチがさらにシンプルな構文で可能になります。たとえば、plum-dispatchを使用すると、
プラム輸入発送@dispatch def collide ( a : Asteroid , b : Asteroid ) -> None : """小惑星が小惑星に衝突したときの動作。""" # ...新しい動作を定義...@dispatch def collide ( a : Asteroid , b : Spaceship ) -> None : """小惑星が宇宙船に衝突したときの動作。""" # ...新しい動作を定義...# ...さらにルールを定義する...C言語には動的ディスパッチ機能がないため、何らかの形で手動で実装する必要があります。多くの場合、列挙型を使用してオブジェクトのサブタイプを識別します。動的ディスパッチは、関数ポインタ分岐テーブルでこの値を参照することで実現できます。以下にC言語での簡単な例を示します。
typedef void ( * CollisionCase )( void );void collisionAsteroidAsteroid ( void ) { // 小惑星同士の衝突を処理する... }void collisionAsteroidSpaceship ( void ) { // 小惑星と宇宙船の衝突を処理する... }void collisionSpaceshipAsteroid ( void ) { // 宇宙船と小惑星の衝突を処理する... }void collisionSpaceshipSpaceship ( void ) { // 宇宙船同士の衝突を処理する... }typedef enum { COLLIDEABLE_ASTEROID = 0 , COLLIDEABLE_SPACESHIP , COLLIDEABLE_COUNT // Collideable 自体の型ではなく、定義されている宇宙オブジェクトの数を見つけるために使用されます} Collideable ;CollisionCase collisionCases [ COLLIDEABLE_COUNT ][ COLLIDEABLE_COUNT ] = { { & collisionAsteroidAsteroid , & collisionAsteroidSpaceship }, { & collisionSpaceshipAsteroid , & collisionSpaceshipSpaceship } };void collide ( Collideable a , Collideable b ) { ( * collisionCases [ a ][ b ])(); }int main ( void ) { collide ( COLLIDEABLE_SPACESHIP , COLLIDEABLE_ASTEROID ); }C Object Systemライブラリ[ 19 ]を使用すると、 CはCLOSと同様の動的ディスパッチをサポートします。これは完全に拡張可能で、メソッドの手動処理は必要ありません。動的メッセージ(メソッド)はCOSのディスパッチャによってディスパッチされ、Objective-Cよりも高速です。COSの例を以下に示します。
#include <stdio.h> #include <cos/Object.h> #include <cos/gen/object.h>// クラスdefclass ( Asteroid ) // データメンバーendclassdefclass ( Spaceship ) // データメンバーendclass// ジェネリクスdefgeneric ( bool , collide_with , _1 , _2 );// マルチメソッドdefmethod ( bool , collide_with , Asteroid , Asteroid ) // 小惑星が小惑星に衝突した場合の処理 endmethoddefmethod ( bool , collide_with , Asteroid , Spaceship ) // 小惑星が宇宙船に衝突した場合の処理 endmethoddefmethod ( bool , collide_with , Spaceship , Asteroid ) // 宇宙船が小惑星に衝突した場合の処理 endmethoddefmethod ( bool , collide_with , Spaceship , Spaceship ) // 宇宙船同士の衝突を処理するendmethod// 使用例int main ( void ) { OBJ a = gnew ( Asteroid ); OBJ s = gnew ( Spaceship );printf ( "<a,a> = %d \n " , collide_with ( a , a )); printf ( "<a,s> = %d \n " , collide_with ( a , s )); printf ( "<s,a> = %d \n " , collide_with ( s , a )); printf ( "<s,s> = %d \n " , collide_with ( s , s ));grelease ( a ); grelease ( s ); }2021年現在C ++ はネイティブでは単一ディスパッチのみをサポートしていますが、2007 年にBjarne Stroustrup (および共同研究者) によってマルチメソッド (マルチディスパッチ) の追加が提案されました。 [ 20 ]この制限を回避する方法は類似しており、ビジターパターン、動的キャスト、またはライブラリのいずれかを使用します。
// dynamic_cast を使用した実行時型比較の例class Collideable { public : virtual void collideWith ( Collideable & other ) = 0 ; };class Asteroid : public Collideable { public : void collideWith ( Collideable & other ) { // dynamic_cast をポインタ型にキャストすると、キャストが失敗した場合に nullptr が返されます// (dynamic_cast をリファレンス型にキャストすると、失敗した場合に例外がスローされます) if ( Asteroid * asteroid = dynamic_cast < Asteroid *> ( & other )) { // 小惑星同士の衝突を処理します} else if ( Spaceship * spaceship = dynamic_cast < Spaceship *> ( & other )) { // 小惑星と宇宙船の衝突を処理します} else { // デフォルトの衝突処理をここに記述します} } };class Spaceship : public Collideable { public : void collideWith ( Collideable & other ) { if ( Asteroid * asteroid = dynamic_cast < Asteroid *> ( & other )) { // Spaceship と Asteroid の衝突を処理} else if ( Spaceship * spaceship = dynamic_cast < Spaceship *> ( & other )) { // Spaceship と Spaceship の衝突を処理} else { // デフォルトの衝突処理} } };またはメソッドへのポインタ参照テーブル:
import std ;std :: unordered_mapを使用します。class Collideable { protected : explicit Collideable ( uint32_t cid ) : tid { cid } {}virtual ~ Collideable () = default ;const uint32_t tid ; // 型IDusing CollisionHandler = void ( Collideable ::* )( Collideable & other ); using CollisionHandlers = unordered_map < uint64_t , CollisionHandler > ;static void addHandler ( uint32_t id1 , uint32_t id2 , CollisionHandler handler ) { collisionCases.insert ( CollisionHandlers :: value_type ( key ( id1 , id2 ) , handler )) ; }static uint64_t key ( uint32_t id1 , uint32_t id2 ) { return uint64_t ( id1 ) << 32 | id2 ; }static inline CollisionHandlers collisionCases {}; public : void collideWith ( Collideable & other ) { if ( auto handler = collisionCases.find ( key ( tid , other.tid ) ); handler != collisionCases.end ( ) ) { ( this - > * handler- > second )( other ); // メソッドへのポインタ呼び出し} else { //デフォルトの衝突処理} } };class Asteroid : public Collideable { private : void asteroidCollision ( Collideable & other ) { // 小惑星同士の衝突を処理する}void spaceshipCollision ( Collideable & other ) { // 小惑星と宇宙船の衝突を処理する} public : Asteroid () : Collideable ( cid ) {}~ Asteroid () =デフォルト;static void initCases (); static inline const uint32_t cid = typeid ( Asteroid ) .hash_code (); };class Spaceship : public Collideable { private : void asteroidCollision ( Collideable & other ) { // 宇宙船と小惑星の衝突を処理する}void spaceshipCollision ( Collideable & other ) { // 宇宙船同士の衝突を処理する} public : Spaceship () : Collideable ( cid ) {}~宇宙船() =デフォルト;static void initCases (); static inline const uint32_t cid = typeid ( Spaceship ) .hash_code (); // クラスID };void Asteroid::initCases () { addHandler ( cid , cid , CollisionHandler ( & Asteroid :: asteroidCollision )); addHandler ( cid , Spaceship :: cid , CollisionHandler ( & Asteroid :: spaceshipCollision )); }void Spaceship::initCases () { addHandler ( cid , Asteroid :: cid , CollisionHandler ( & Spaceship :: asteroidCollision )); addHandler ( cid , cid , CollisionHandler ( & Spaceship :: spaceshipCollision )); }int main ( int argc , char * argv []) { Asteroid :: initCases (); Spaceship :: initCases ();小惑星a1 ;小惑星a2 ;宇宙船s1 ;宇宙船s2 ;a1.collideWith ( a2 ) ; a1.collideWith ( s1 ) ;s1.collideWith ( s2 ) ; s1.collideWith ( a1 ) ; }YOMM2ライブラリ[ 21 ]は、オープンマルチメソッドの高速で直交的な実装を提供します。
オープンメソッドを宣言するための構文は、ネイティブC++実装の提案にヒントを得ています。このライブラリでは、仮想引数として使用されるすべてのクラス(およびそのサブクラス)をユーザーが登録する必要がありますが、既存のコードを変更する必要はありません。メソッドは通常のインラインC++関数として実装され、オーバーロードが可能で、ポインタで渡すこともできます。仮想引数の数に制限はなく、非仮想引数と自由に混在させることができます。
このライブラリは、圧縮ディスパッチテーブルや衝突のない整数ハッシュテーブルといった複数の技術を組み合わせることで、メモリ使用量を抑えつつ、メソッド呼び出しを定数時間で実行します。最新の最適化コンパイラを使用した場合、単一の仮想引数を持つオープンメソッドへの呼び出しにかかる時間は、通常の仮想メンバ関数を呼び出す場合と比べてわずか15~30%しか変わりません。
小惑星の例は、次のように実装できます。
#include <yorel/yomm2/keywords.hpp>import std ;std :: unique_ptrを使用します。class Collideable { public : virtual ~ Collideable () = default ; };class Asteroid : public Collideable { // ... };class Spaceship : public Collideable { // ... };register_classes ( Collideable 、Spaceship 、Asteroid );declare_method ( void , collideWith , ( virtual_ < Collideable &> , virtual_ < Collideable &> ));define_method ( void , collideWith , ( Collideable & left , Collideable & right )) { // デフォルトの衝突処理}define_method ( void , collideWith , ( Asteroid & left , Asteroid & right )) { // 小惑星同士の衝突を処理する}define_method ( void , collideWith , ( Asteroid & left , Spaceship & right )) { // 小惑星と宇宙船の衝突を処理する}define_method ( void , collideWith , ( Spaceship & left , Asteroid & right )) { // 宇宙船と小惑星の衝突を処理する}define_method ( void , collideWith , ( Spaceship & left , Spaceship & right )) { // 宇宙船同士の衝突を処理する}int main ( int argc , char * argv []) { yorel :: yomm2 :: update_methods ();unique_ptr < Collideable > a1 ( std :: make_unique < Asteroid > ()); unique_ptr < Collideable > a2 ( std :: make_unique < Asteroid > ()); unique_ptr < Collideable > s1 ( std :: make_unique < Spaceship > ()); unique_ptr < Collideable > s2 ( std :: make_unique < Spaceship > ()); // 注: 型は部分的に消去されていますcollideWith ( * a1 , * a2 ); // 小惑星同士の衝突collideWith ( * a1 , * s1 ); // 小惑星と宇宙船の衝突collideWith ( * s1 , * a1 ); // 宇宙船と小惑星の衝突collideWith ( * s1 , * s2 ); // 宇宙船同士の衝突return 0 ; }ストロストルップは『C++ の設計と進化』の中で、マルチメソッドの概念は気に入っており、C++ に実装することを検討したが、効率的なサンプル実装 (仮想関数に匹敵するもの) を見つけることができず、いくつかの型曖昧性の問題を解決できなかったと述べている。そして、この機能は依然としてあれば良いものの、上記の C/C++ の例で概説したように、ダブルディスパッチまたは型ベースのルックアップテーブルを使用して近似的に実装できるため、将来の言語改訂では優先度の低い機能であると述べている。[ 22 ]
2021年現在他の多くのオブジェクト指向プログラミング言語と同様に、D はネイティブではシングルディスパッチのみをサポートしています。ただし、D ではライブラリ関数としてオープンマルチメソッドをエミュレートすることが可能です。openmethodsライブラリ[ 23 ]はその一例です。
// 宣言Matrix plus ( virtual ! Matrix , virtual ! Matrix );// 2 つの DenseMatrix オブジェクトに対するオーバーライド@method Matrix _plus ( DenseMatrix a , DenseMatrix b ) { const int nr = a . rows ; const int nc = a . cols ; assert ( a . nr == b . nr ); assert ( a . nc == b . nc ); auto result = new DenseMatrix ; result . nr = nr ; result . nc = nc ; result . elems . length = a . elems . length ; result . elems [] = a . elems [] + b . elems []; return result ; }// 2 つの DiagonalMatrix オブジェクトに対するオーバーライド@method Matrix _plus ( DiagonalMatrix a , DiagonalMatrix b ) { assert ( a . rows == b . rows ); double [] sum ; sum . length = a . elems . length ; sum [] = a . elems [] + b . elems []; return new DiagonalMatrix ( sum ); }Javaのような単一ディスパッチしか持たない言語では、複数のレベルの単一ディスパッチによって多重ディスパッチをエミュレートできます。
![]()
interface Collideable { void collideWith ( Collideable other );// メソッドオーバーロードのない言語では、これらのメソッドには異なる名前が必要になります。void collideWith ( Asteroid asteroid ); void collideWith ( Spaceship spaceship ); }class Asteroid implements Collideable { public void collideWith ( Collideable other ) { // 他のオブジェクトに対してcollideWithを呼び出す。other . collideWith ( this ); }public void collideWith ( Asteroid asteroid ) { // 小惑星同士の衝突を処理する。}public void collideWith ( Spaceship spaceship ) { // 小惑星と宇宙船の衝突を処理する。} }class Spaceship implements Collideable { public void collideWith ( Collideable other ) { // 他のオブジェクトに対してcollideWithを呼び出す。other . collideWith ( this ); }public void collideWith ( Asteroid asteroid ) { // 宇宙船と小惑星の衝突を処理する。}public void collideWith ( Spaceship spaceship ) { // 宇宙船同士の衝突を処理する。} }実行時instanceofチェックを、一方または両方のレベルで使用することもできます。
多重ディスパッチ(2 つ以上の引数の動的な型に基づいて呼び出す関数を選択すること)は、オブジェクト指向プログラミングにおけるいくつかの古典的な問題に対する解決策です。