仮想関数テーブル仮想関数テーブル(かそうかんすうテーブル、英: virtual method table)あるいはvtableは、プログラミング言語の実装において動的なポリモーフィズム、すなわち実行時のメソッドの束縛を実現するために用いられる機構である。 あるプログラムが、継承関係にある複数のクラス(データ型)を持っているとする。たとえばスーパークラス プログラムが このような動的な割り当てを実現するには様々な方法があるが、C++および関連するプログラミング言語(D言語、Java、C#など)では、vtableによる方法が一般的である。 オブジェクトのインターフェイスを実装から分離する言語(Visual Basic や Delphi など)でも、異なるメソッドポインタの集合を用いるだけで、異なる実装をオブジェクトが使用できるため、vtable による方法を採用する傾向にある。 C言語はオブジェクト指向プログラミングの機能を言語仕様としてサポートしないが、関数ポインタを利用することで仮想関数を模倣することができる。 vtable の実装
C++ の標準では、動的なディスパッチがどのように実装されるべきかについて規定していないが、一般的にコンパイラは若干の変更を加えて共通の基本的なモデルを用いる。 典型的には、コンパイラは個々のクラスごとに別の vtable を作成する。オブジェクトが生成される際、vtable へのポインタ(仮想テーブルポインタ, vpointer, vptr)がオブジェクトの不可視のメンバー(フィールド)として追加される(通常は最初のメンバーとなる)。コンパイラはコンストラクタ内に"隠れた"コードを生成し、クラスのオブジェクトの vpointer が、対応する vtable のアドレスで初期化されるようにする。 例下記のクラスの宣言は C++ の文法に従うものとする: class B1
{
public:
void f0() {}
virtual void f1() {}
int int_in_b1;
};
class B2
{
public:
virtual void f2() {}
int int_in_b2;
};
これらは下記のクラスを派生させる。 class D : public B1, public B2
{
public:
virtual void d() {}
virtual void f2() {} // B2::f2() をオーバーライド
int int_in_d;
};
B2 *b2 = new B2();
D *d = new D();
GNUコンパイラコレクション の g++ 3.4.6 は、オブジェクト b2: +0: pointer to virtual method table of B2 +4: value of int_in_b2 virtual method table of B2: +0: B2::f2() +4: B2::~B2() オブジェクト d: +0: pointer to virtual method table of D (for B1) +4: value of int_in_b1 +8: pointer to virtual method table of D (for B2) +12: value of int_in_b2 +16: value of int_in_d virtual method table of D (for B1): +0: B1::f1() +4: D::~D() +8: D::d() +12: D::f2() virtual method table of D (for B2): +0: D::d() +4: D::f2() // B2::f2() is overridden by D::f2() +8: D::~D() 仮想でない関数( クラス 多重継承とthunkg++ コンパイラはクラス 下記のような C++ コードを考える: D *d = new D();
B1 *b1 = dynamic_cast<B1*>(d);
B2 *b2 = dynamic_cast<B2*>(d);
呼び出し呼び出しの際には、 単一継承(あるいは、単一継承のみ可能な言語)の場合、vpointer が常に *((*d)[0])(d)
より一般的なケースでは、上記のような *((d->/* Dの(B1用の)仮想関数テーブルへのポインタ*/)[0])(d)
*((d->/* Dの(B1用の)仮想関数テーブルへのポインタ*/)[12])(d)
*((d->/* Dの(B2用の)仮想関数テーブルへのポインタ*/)[0])(d+8)
これに対して、 *B1::f0(d)
効率単なるコンパイルされたポインタへのジャンプである非仮想関数の呼び出しに対して、仮想関数の呼び出しは最低一度以上、余分にポインタをたどる操作や"fixup" が必要である。そのため、仮想関数の呼び出しは原理的に非仮想の関数呼び出しに対して低速である。実験によれば 6-13% の実行時間が単なる関数のディスパッチに用いられ、オーバーヘッドは場合によって 50% に達する[1]。 さらに、 JIT コンパイルが使用できない環境では、仮想関数は通常インライン展開できない。テーブルの参照を行う部分を、たとえばインライン化された本体部分を条件文で実行させることも可能ではあるが、そうした最適化は一般的ではない。 オーバーヘッドを避けるため、コンパイラはコンパイル時に呼び出しが解決できる場合には vtable の生成を行わない。 従って、上記の 比較、およびその他の方法vtable は一般的に動的なディスパッチを実現するための、性能上のよいトレードオフであるが、たとえば二分木ディスパッチ[2]といった代替の方法も存在する。 しかし、vtable は特殊な "this" パラメータでは single dispatch のみ考慮しており、ディスパッチの際全てのパラメータの型が考慮される多重ディスパッチ(Common Lisp や Julia、Dylan)とは異なる。 vtables はまた、コンパイル時に単一の配列にメソッドを配置するため、ディスパッチが既知のメソッドのセットに限定されている場合のみうまく動作する。これはダック・タイピング言語(Smalltalk、Python、JavaScript、あるいは C++ のコンパイル時のテンプレート機構)とは対照的である。 上記の一つまたは両方をサポートする言語は、ディスパッチをハッシュテーブルの文字列検索や同等の手段で行うことが多い。ディスパッチを高速化する様々な方法があり(たとえば、メソッドの名前を intern 化やトークン化する、検索のキャッシュ、JITコンパイルなど)、ディスパッチの時間は全体的な処理時間にそれほどの影響を与えない。それでもなお、vtable の検索の方が明らかに高速である。また vtable は実装やデバッグが簡単で、文字列のハッシュテーブルよりも"Cの精神"に近い。 脚注注釈出典
関連項目参考文献
|