コンピュータサイエンスにおいて、動的ディスパッチとは、実行時に多態性操作(メソッドまたは関数)のどの実装を呼び出すかを選択するプロセスです。これは、オブジェクト指向プログラミング(OOP)言語およびシステムで一般的に使用され、その主要な特徴と考えられています。[ 1 ]
オブジェクト指向システムは、名前で参照される操作を実行する相互作用するオブジェクトの集合として問題をモデル化します。ポリモーフィズムとは、ある程度互換性のあるオブジェクトがそれぞれ同じ名前の操作を公開するものの、動作が異なる可能性がある現象です。たとえば、FileオブジェクトとDatabaseオブジェクトはどちらも、人事記録をストレージに書き込むために使用できるStoreRecordメソッドを持っています。それらの実装は異なります。プログラムは、 FileオブジェクトまたはDatabaseオブジェクトのいずれかであるオブジェクトへの参照を保持します。どちらであるかは実行時設定によって決定されている場合があり、この段階では、プログラムはどちらであるかを知らないか、気にしない場合があります。プログラムがオブジェクトのStoreRecord を呼び出すと、どの動作を実行するかを選択する何かが必要になります。OOP をオブジェクトへのメッセージの送信と考えると、この例では、プログラムは未知の型のオブジェクトにStoreRecordメッセージを送信し、メッセージを適切なオブジェクトにディスパッチするのは実行時サポートシステムに任せます。オブジェクトは、実装されている動作を実行します。[ 2 ]
動的ディスパッチは、コンパイル時に多態性操作の実装が選択される静的ディスパッチとは対照的です。動的ディスパッチの目的は、パラメータ(または複数のパラメータ)の実行時型が判明するまで、適切な実装の選択を延期することです。
動的ディスパッチは、遅延バインディング(動的バインディングとも呼ばれる)とは異なります。名前バインディングは、名前を操作に関連付けます。ポリモーフィック操作には複数の実装があり、すべて同じ名前に関連付けられます。バインディングはコンパイル時、または(遅延バインディングの場合は)実行時に行うことができます。動的ディスパッチでは、実行時に操作の特定の実装が選択されます。動的ディスパッチは遅延バインディングを意味するものではありませんが、遅延バインディングは動的ディスパッチを意味します。なぜなら、遅延バインドされた操作の実装は実行時までわからないからです。
呼び出すメソッドのバージョンの選択は、単一のオブジェクトに基づく場合と、オブジェクトの組み合わせに基づく場合があります。前者はシングルディスパッチと呼ばれ、 Smalltalk、C++、Java、C#、Objective-C、Swift、JavaScript、Pythonなどの一般的なオブジェクト指向言語で直接サポートされています。これらの言語や類似の言語では、次のような構文で除算のメソッドを呼び出すことができます。
配当金.除数) #配当金 / 除数ここで、パラメータはオプションです。これは、divideという名前のメッセージをdivisorパラメータ付きでdividendに送信することとみなされます。実装は、 divisorの型や値に関係なく、 dividendの型 (おそらく有理数、浮動小数点数、行列)のみに基づいて選択されます。
対照的に、一部の言語では、オペランドの組み合わせに基づいてメソッドや関数がディスパッチされます。除算の場合、被除数と除数の型によって、どの除算演算が実行されるかが決まります。これは多重ディスパッチと呼ばれます。多重ディスパッチをサポートする言語の例としては、Common Lisp、Dylan、Juliaなどがあります。
言語は、異なる動的ディスパッチ機構で実装される場合がある。言語が提供する動的ディスパッチ機構の選択は、その言語内で利用可能な、あるいは最も自然に使用できるプログラミングパラダイムを大きく変える。
通常、型付き言語では、ディスパッチ機構は引数の型に基づいて実行されます(最も一般的には、メッセージの受信側の型に基づきます)。型付けが弱い、あるいは型付けのない言語では、各オブジェクトのオブジェクトデータの一部としてディスパッチテーブルが保持されることがよくあります。これにより、インスタンスごとに異なる動作が可能になり、各インスタンスは特定のメッセージを別のメソッドにマッピングできます。
一部の言語では、ハイブリッドなアプローチが採用されている。
動的ディスパッチは必ずオーバーヘッドを伴うため、一部のプログラミング言語では特定のメソッドに対して静的ディスパッチを提供しています。
C++ は早期バインディングを使用し、動的ディスパッチと静的ディスパッチの両方を提供します。デフォルトのディスパッチ形式は静的です。動的ディスパッチを使用するには、プログラマはメソッドを仮想として宣言する必要があります。
C++コンパイラは通常、動的ディスパッチを仮想関数テーブル(vtable)と呼ばれるデータ構造で実装します。このvtableは、特定のクラスの名前と実装のマッピングをメンバ関数ポインタのセットとして定義します。これは純粋に実装の詳細であり、C++仕様ではvtableについては言及されていません。この型のインスタンスは、インスタンスデータの一部としてこのテーブルへのポインタを格納するため、多重継承が使用されるシナリオが複雑になります。C++は遅延バインディングをサポートしていないため、C++オブジェクトの仮想テーブルは実行時に変更できず、ディスパッチ対象はコンパイル時に選択された有限のセットに制限されます。
C++では、型オーバーロードによって動的ディスパッチは生成されません。これは、C++言語がメッセージパラメータの型を正式なメッセージ名の一部とみなすためです。つまり、プログラマーが目にするメッセージ名は、バインディングに使用される正式な名前とは異なります。
Go、Rust、Nimでは、より汎用性の高い早期バインディングのバリエーションが使用されています。 Vtableポインタはオブジェクト参照とともに「ファットポインタ」(Goでは「インターフェース」、Rustでは「トレイトオブジェクト」[ 3 ] [ 4 ])として保持されます。
これにより、サポートされるインターフェースと基盤となるデータ構造が分離されます。コンパイルされたライブラリは、型を正しく使用するために、サポートされているインターフェースの全範囲を知る必要はなく、必要な特定の仮想テーブルレイアウトだけを知っていればよいのです。コードは、同じデータに対して異なるインターフェースを異なる関数に渡すことができます。この汎用性は、オブジェクト参照ごとに余分なデータが発生するという代償を伴います。このような参照が多数永続的に保存される場合、これは問題となります。
ファットポインタという用語は、追加の関連情報を持つポインタを指します。追加情報は、前述の動的ディスパッチ用のvtableポインタである場合もありますが、より一般的には、スライスなどを表す関連オブジェクトのサイズです。
Smalltalkは型ベースのメッセージディスパッチャを使用します。各インスタンスは単一の型を持ち、その型の定義にはメソッドが含まれています。インスタンスがメッセージを受信すると、ディスパッチャはその型に対応するメッセージとメソッドのマッピングから該当するメソッドを検索し、そのメソッドを呼び出します。
型は基底型の連鎖を持つことができるため、このルックアップはコストがかかる可能性があります。Smalltalkのメカニズムを単純に実装すると、C++の実装よりもかなり高いオーバーヘッドが発生し、オブジェクトが受信するメッセージごとにこのオーバーヘッドが発生します。
実際のSmalltalkの実装では、メソッドのディスパッチを非常に高速化するインラインキャッシング[ 5 ]と呼ばれる手法がよく使われます。インラインキャッシングは基本的に、呼び出し元のメソッドのアドレスとオブジェクトクラス(マルチウェイキャッシングの場合は複数のペア)を保存します。キャッシュされたメソッドは、メソッドセレクタに基づいて、最も一般的なターゲットメソッド(またはキャッシュミスハンドラ)で初期化されます。実行中にメソッド呼び出しサイトに到達すると、キャッシュ内のアドレスが呼び出されます。(動的コードジェネレータでは、キャッシュミスロジックによって直接アドレスがバックパッチされるため、この呼び出しは直接呼び出しになります。)呼び出されたメソッドのプロローグコードは、キャッシュされたクラスと実際のオブジェクトクラスを比較し、一致しない場合は、実行はキャッシュミスハンドラに分岐して、クラス内の正しいメソッドを見つけます。高速な実装では、複数のキャッシュエントリを持つことができ、最初のキャッシュミスで正しいメソッドに実行を到達させるのに必要な命令はわずか数個です。一般的なケースは、キャッシュされたクラスが一致する場合で、実行はメソッド内で続行されます。
アウトオブラインキャッシュは、オブジェクトクラスとメソッドセレクタを使用して、メソッド呼び出しロジック内でも利用できます。ある設計では、クラスとメソッドセレクタをハッシュ化し、メソッドディスパッチキャッシュテーブルへのインデックスとして使用します。
Smalltalkはリフレクティブ言語であるため、多くの実装では、個々のオブジェクトを動的に生成されたメソッドルックアップテーブルを持つオブジェクトにミューテーションすることが可能です。これにより、オブジェクトごとに動作を変更できます。このことから、プロトタイプベース言語と呼ばれる言語群が生まれ、その中でもSelfとJavaScriptが最も有名です。メソッドディスパッチのキャッシュを慎重に設計することで、プロトタイプベース言語でも高性能なメソッドディスパッチを実現できます。
Python、Ruby、Objective-C、Groovyなど、他の多くの動的型付け言語も同様のアプローチを採用している。
![]()
C言語には他の言語のように動的ディスパッチ機能が組み込まれていませんが、関数ポインタを手動で管理することで実現可能です。
#include <stdio.h> #include <stdlib.h>typedef struct Pet { const char * name ; void ( * speak )( struct Pet * ); // 関数ポインタを保持} Pet ;Pet * createPet ( const char * name , void ( * speakFunc )( Pet * )) { Pet * pet = ( Pet * ) malloc ( sizeof ( Pet )); pet -> name = name ; pet -> speak = speakFunc ; return pet ; }void destroyPet ( Pet * pet ) { free ( pet ); }void dogSpeak ( Pet * pet ) { printf ( "%s says 'Woof!' \n " , pet- > name ); }void catSpeak ( Pet * pet ) { printf ( "%s says 'Meow!' \n " , pet- > name ); }void speak ( Pet * pet ) { pet -> speak ( pet ); }int main () { Pet * fido = createPet ( "Fido" , dogSpeak ); Pet * simba = createPet ( "Simba" , catSpeak );speak ( fido ); // dogSpeak() を呼び出すspeak ( simba ); // catSpeak() を話す// クリーンアップ; リソースを解放destroyPet ( fido ); destroyPet ( simba ); return 0 ; }import std ;std :: stringを使用します。// Pet を抽象仮想基底クラスにするclass Pet { protected : string name ; public : explicit Pet ( const string & name ) : name { name } {}virtual void speak () = 0 ; };class Dog : public Pet { public : explicit Dog ( const string & name ) : Pet ( name ) {}void speak () override { std :: println ( "{} says 'Woof!'" , name ); } };class Cat : public Pet { public : explicit Cat ( const string & name ) : Pet ( name ) {}void speak () override { std :: println ( "{} says 'Meow!'" , name ); } };// speak() は Pet から派生したあらゆるものを受け入れることができますvoid speak ( Pet & pet ) { pet . speak (); }int main () { Dog fido ( "Fido" ); Cat simba ( "Simba" ); speak ( fido ); speak ( simba ); return 0 ; }名前空間Wikipedia.Examples ;Systemを使用します。abstract class Pet { protected string name ;public Pet ( string name ) { this.name = name ; }public abstract void Speak (); }class Dog : Pet { public Dog ( string name ) : base ( name ) { }public override void Speak () { Console.WriteLine ( $ "{name} says 'Woof!'" ) ; } }class Cat : Pet { public Cat ( string name ) : base ( name ) { }public override void Speak () { Console.WriteLine ( $ "{name} says 'Meow!'" ) ; } }public class Main { public static void Speak ( Pet pet ) { pet . Speak (); }public static void Main () { Dog fido = new ( "Fido" ); Cat simba = new ( "Simba" ); Speak ( fido ); Speak ( simba ); } }パッケージorg.wikipedia.examples ;abstract class Pet { protected String name ;public Pet ( String name ) { this.name = name ; }public abstract void speak (); }class Dog extends Pet { public Dog ( String name ) { super ( name ); }@Override public void speak () { System.out.printf ( " % s says ' Woof!'%n" , name ) ; } }class Cat extends Pet { public Cat ( String name ) { super ( name ); }@Override public void speak () { System . out . printf ( "%s says 'Meow!'%n" , name ); } };public class Main { public static void speak ( Pet pet ) { pet . speak (); }public static void main ( String [] args ) { Dog fido = new Dog ( "Fido" ); Cat simba = new Cat ( "Simba" ); speak ( fido ); speak ( simba ); } }from abc import ABC , abstractmethod from typing import Never# ABC は、それを直接継承するクラスが抽象クラスであることを示すために使用されるクラスです。class Pet ( ABC ): def __init__ ( self , name : str ) -> None : self . name = name@abstractmethod def speak ( self ) -> Never : raise NotImplementedError ( "抽象メソッドは派生クラスで実装する必要があります" )class Dog ( Pet ): def __init__ ( self , name : str ) -> None : super . __init__ ( name )def speak ( self ) -> None : print ( f " { self.name }は「ワン!」と言っています" )class Cat ( Pet ): def __init__ ( self , name : str ) -> None : super . __init__ ( name )def speak ( self ) -> None : print ( f " { self.name }は「ニャー!」と言っています" )def speak ( pet : Pet ) -> None : # speak メソッドを動的にディスパッチします# pet は Dog または Cat のインスタンスのいずれかですpet . speak ()if __name__ == "__main__" : fido : Dog = Dog ( "Fido" ) speak ( fido ) simba : Cat = Cat ( "Simba" ) speak ( simba )trait Pet { fn speak ( & self ); }struct Dog <' a > { name : & ' a str }struct Cat <' a > { name : & ' a str }impl <' a > Dog <' a > { fn new ( name : & ' a str ) -> Self { Dog { name } } }impl <' a > Cat <' a > { fn new ( name : & ' a str ) -> Self { Cat { name } } }impl <' a > Pet for Dog <' a > { fn speak ( & self ) { println! ( "{} says 'Woof!'" , self . name ); } }impl <' a > Pet for Cat <' a > { fn speak ( & self ) { println! ( "{} says 'Meow!'" , self . name ); } }// speak() は動的ディスパッチを使用し、実行時に型を解決します。// Pet トレイトを実装する任意の型に対してfn speak ( pet : & dyn Pet ) { pet . speak (); }fn main () { let fido : Dog = Dog :: new ( "Fido" ); let simba : Cat = Cat :: new ( "Simba" ); speak ( & fido ); speak ( & simba ); }トレイトオブジェクトは動的ディスパッチを実行します […] トレイトオブジェクトを使用する場合、Rust は動的ディスパッチを使用する必要があります。コンパイラは、トレイトオブジェクトを使用するコードで使用される可能性のあるすべての型を知っているわけではないため、どの型に実装されているどのメソッドを呼び出すべきかわかりません。代わりに、実行時に、Rust はトレイトオブジェクト内のポインタを使用して、呼び出すメソッドを特定します。このルックアップには、静的ディスパッチでは発生しない実行時コストがかかります。また、動的ディスパッチは、コンパイラがメソッドのコードをインライン化することを選択するのを防ぎ、結果として一部の最適化を妨げます。(xxix+1+527+3ページ)
[…] Geos が 16 個の割り込みを必要とする理由は
、
コードのサイズを変更せずに、セグメント間 ("far") 関数呼び出しを割り込みに変換するスキームが使用されているためです。これは、Geos アプリケーションによって行われるすべてのセグメント間呼び出しに「何か」(カーネル) がフックして、適切なコードセグメントが
仮想メモリ
からロードされ、ロックダウンされていることを確認するためです。DOS 用語で言えば、これは
オーバーレイ
ローダー
に相当します
が、コンパイラやアプリケーションからの明示的なサポートを必要とせずに追加できます。次のようなことが起こります。 […] 1. リアルモードコンパイラは、次のような命令を生成します。 CALL
<segment>:<offset>
-> 9A <offlow><offhigh><seglow><seghigh> ここで、<seglow><seghigh> は通常、コードが配置されているアドレスに応じてロード時に固定する必要のあるアドレスとして定義されます。 […] 2. Geos リンカはこれを別のものに変換します。 INT 8xh -> CD 8x […] DB <seghigh>,<offlow>,<offhigh> […] これは再び 5 バイトなので、「その場で」固定できることに注意してください。問題は、割り込みには 2 バイトが必要なのに対し、CALL FAR 命令には 1 バイトしか必要ないことです。結果として、32 ビットのベクトル (<seg><ofs>) を 24 ビットに圧縮する必要があります。 […] これは 2 つの方法で実現されます。まず、<seg> アドレスはセグメントへの「ハンドル」としてエンコードされ、その最下位の 4
ビット
は常にゼロです。これにより 4 ビットが節約されます。さらに […] 残りの 4 ビットは割り込みベクタの下位 4 ビットに入り、INT 80h から 8Fh までの値を生成します。 […] これらのベクタの割り込みハンドラはすべて同じです。3.5 バイト表記からアドレスを「展開」し、セグメントの絶対アドレスを検索し、仮想メモリのロード処理を行った後、呼び出しを転送します。呼び出しからの戻りも、対応するロック解除コードを経由します。 […] 割り込みベクタの下位 4 ビット (80h–8Fh) には、セグメント ハンドルのビット 4 から 7 が格納されます。セグメント ハンドルのビット 0 から 3 は (Geos ハンドルの定義により) 常に 0 です。 […] すべての Geos API は「オーバーレイ」スキームで実行されます […]: Geos アプリケーションがメモリにロードされると、ローダはシステム ライブラリの関数への呼び出しを対応する INT ベースの呼び出しに自動的に置き換えます。いずれにせよ、これらは一定ではなく、ライブラリのコード セグメントに割り当てられたハンドルに依存します。[…] Geos は当初
、非常に早い段階で
保護モードに変換する予定でした […]、
リアルモード
単なる「レガシーオプション」であるにもかかわらず、アセンブリコードのほぼすべての行がそれに対応できるようになっている[…]
{{cite newsgroup}}: CS1メンテナンス: アーカイブサービスは非推奨になりました (リンク)[…] このようなマングルされたポインタの場合 […] 何年も前に、アクセルと私は、複数の割り込みベクタに対してドライバへの *1* エントリ ポイントを使用する方法を考えていました (これにより、複数のエントリ ポイントと、それらすべてでほぼ同じ起動/終了フレーム コードのためのスペースを大幅に節約できるため)。その後、内部で異なる割り込みハンドラに切り替えます。例えば、1234h:0000h […] 1233h:0010h […] 1232h:0020h […] 1231h:0030h […] 1230h:0040h […] はすべてまったく同じエントリ ポイントを指しています。 INT 21h を 1234h:0000h に、INT 2Fh を 1233h:0010h にフックすると、すべて同じ「ループホール」を通りますが、それでもそれらを区別して内部で異なるハンドラに分岐することができます。 HMA ロード用のA20スタブへの「圧縮された」エントリ ポイントを考えてみてください。 これは、プログラムがセグメント:オフセット マジックを開始しない限り機能します。 […] これとは反対に、複数のエントリ ポイントを持つアプローチ ( IBMの割り込み共有プロトコルをサポートする場合もある) と比較してください。このアプローチでは、多くの割り込みをフックすると、はるかに多くのメモリを消費します。 […] 他のドライバがポインタを正規化または非正規化する理由が何であれ、実際には安全ではない可能性が高いという結論に至りました。 […](注:これは、特にインテルのx86プロセッサにおけるリアルモードのセグメント:オフセットアドレッシング用の「ファットポインタ」に類似したもので、共有コードのエントリポイントへの意図的に非正規化されたポインタと、共有コード内の異なる呼び出し元を区別するための情報の両方を含んでいます。オープンシステムでは、公開インターフェース上でポインタを正規化するサードパーティのインスタンス(他のドライバやアプリケーション内)を完全に排除することはできませんが、この方式は冗長なエントリコードシーケンスを回避するために、内部インターフェース上で安全に使用できます。)