Dynamic Binding & VTable Concept in C++

たまにc++コードのコンパイル時にエラー文言でvtableというキーワードを見たことはないだろうか?Polymorphismという有名なワードに対しこのvtableはあまりにも目立たない存在だ。とはいえPolymorphismを実現するためになくてはならない(最)重要なポジションを占めているのがvtable。別にvtableを理解しなくともPolymorphismの理解はできる。 ただし、骨太になりたいのならばPolymorphismを実現するためにどのようにvtableが使われているのかを理解しておくべきである。

UPCASTING

Virtual関数との比較のためにVirtual関数を持たないクラス継承を説明する。以下のようにvirtual関数を持たないSuperClassをSubClassが継承する。 SuperClassを継承したSubClassの参照をSuperClassポインタ変数に入れた場合は、子クラスへの参照がUPCASTされ親クラスへの参照として扱われる。実行結果はSuperClassへの参照へとUPCASTされるのでSuperClassの関数の内容が表示される。

#include <iostream>
using std::cout;
using std::endl;
class SuperClass {
public:
    void func() { cout << "SuperClass::func()" << endl; }
};
class SubClass : public SuperClass {
public:
    void func() { cout << "SubClass::func()" << endl; }
};
int main(int argc, char **argv ) {
     SubClass sub;
     SuperClass *super =&sub;
     super->func();
     return 0;
}
[実行結果]
SuperClass::func()

VIRTUAL FUNCTION, VTABLE, and VPOINTER

次にVirtual関数の例を説明する。さきほどの例ではUPCASTによりSubClassクラスではなくSuperClassクラスのfunc関数がcallされた。ここではSuperClassの関数にvirtualをつけた場合のfunc関数の振る舞いを確かめてみる。virtual関数func1、func2をもつSuperClassを継承した3つのSubClassを用意する。これら3つはそれぞれfunc1、func2両方とも、func1のみ、func2のみをoverrideしている。 実行結果はSubClassでvirtual関数をoverrideした場合はその内容が、そうでない場合はSuperClassの内容が表示される。

#include <iostream>
using std::cout;
using std::endl;
class SuperClass {
public:
    virtual void funcA() { cout << "SuperClass::funcA()" << endl; };
    virtual void funcB() { cout << "SuperClass::funcB()" << endl; };
};
class SubClass1 : public SuperClass {
public:
    void funcA() { cout << "SubClass1::funcA()" << endl; }
    void funcB() { cout << "SubClass1::funcB()" << endl; }
};
class SubClass2 : public SuperClass {
public:
    void funcA() { cout << "SubClass2::funcA()" << endl; }
};
class SubClass3 : public SuperClass {
public:
    void funcB() { cout << "SubClass2::funcB()" << endl; }
};
int main(int argc, char **argv ) {
    SubClass1 sub1; SubClass2 sub2; SubClass3 sub3;
    cout <<  "***************SubClass1***************" <<  endl;
    SuperClass *super =&sub1;
    super->funcA(); super->funcB();
    cout << "***************SubClass2***************" << endl;
    super =&sub2;
    super->funcA(); super->funcB();
    cout << "***************SubClass3***************" << endl;
    super =&sub3;
    super->funcA();super->funcB();
    return 0;
}
[実行結果]
***************SubClass1***************
SubClass1::funcA()
SubClass1::funcB()
***************SubClass2***************
SubClass2::funcA()
SuperClass::funcB()
***************SubClass3***************
SuperClass::funcA()
SubClass2::funcB()

virtualな関数の場合はコンパイラは型ではなくてオブジェクトのポインタを見ていて実行時にどのfuncが実行されるべきかを判断する。非virtual関数の時と違いコンパイラーはコンパイル時にはどのfuncが呼ばれるのか判別できない。この実行時にどのfuncが呼ばれるのか決定することをDynamic Bindingと呼ぶ。そしてこのDynamic BindingはVirtual Function table(Vtable)と呼ばれるメカニズムによって実現される。ようやく本題。

Vtableとはvirtual関数を持っているクラスや親クラスで定義されているvirtual関数をoverrideしたクラスに対してコンパイラーが作成する(その名のとおり)仮想テーブルである。コンパイラーはvirtual関数を持っている/virtaul関数をoverrideしているクラスにのみクラスごとのVtableを作成してその中にbindすべき関数ポインターを持っている。
またこのVtableを指すポインタのことをvpointerと呼ぶ。コンパイラはVtableを持っているクラスに対してvpointerを隠しメンバー変数として追加する。さらにコンストラクタにそのvpointer変数の初期化を行うコードを追加する。よってオブジェクトが作成されるとき隠しメンバー変数vpointerは対応するVtableアドレスで初期化され、実行時の実行関数の決定は内部でvpointerを通じてVtableをlookupすることで実現される。 (参照: wikipedia:vtable implementation

Vtableに作成される関数ポインターは、そのクラスで持っているvirtual関数のポインター、親クラスで定義されているvirtaul関数をoverrideした関数のポインター、また親クラスでvirtual定義されている関数をoverrideしない場合はその親のvirtual関数ポインタが含まれることになる。上記サンプルにあるSuperClass, SubClass[1-3]に対するオブジェクトとvpointer、vtableとそのvtableに含まれる関数ポインターの関係を図にすると以下のようになる。

vtable - Virtual Function Table

上図を元にサンプル中のSubClass1::funcAのDynamic Bindingイメージを式化してみると次のような感じになる・ vptr1はSubClass1のvpointerとする。 あくまでイメージであり(vptr->)は実際は見えません。

SubClass1 sub1;
SuperClass *super =&sub1;
super->vptr->funcA();    // SubClass1::funcA(),

g++ -fdump-class-hierarchのダンプ結果

最後にg++の-fdump-class-hierarchオプションによるVtableのダンプ結果を見てみる。上記サンプルファイルをvtable.cppとして次のようにコンパイルを行う。 (参考: Options for Debugging Your Program or GCC

 g++ -fdump-class-hierarchy vtable.cc -o vtable

これでコンパイルが終わりvtable実行ファイルができあがる。また同一ディレクトリにvtable.cpp.002t.classという名前のファイルが出来上がる。このファイルにVtableのダンプ結果が出力されている。各クラスのVtable中を見るといまいち意味のわからないものはあるが上図のとおりの関数が含まれており、また各クラスにはvptrを見つけることができる。

Vtable for SubClass1
SubClass1::_ZTV9SubClass1: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI9SubClass1)
8     SubClass1::funcA
12    SubClass1::funcB

Class SubClass1
   size=4 align=4
   base size=4 base align=4
SubClass1 (0xb7254a80) 0 nearly-empty
    vptr=((& SubClass1::_ZTV9SubClass1) + 8u)
  SuperClass (0xb707c1e0) 0 nearly-empty
      primary-for SubClass1 (0xb7254a80)

Vtable for SubClass2
SubClass2::_ZTV9SubClass2: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI9SubClass2)
8     SubClass2::funcA
12    SuperClass::funcB

Class SubClass2
   size=4 align=4
   base size=4 base align=4
SubClass2 (0xb7254b80) 0 nearly-empty
    vptr=((& SubClass2::_ZTV9SubClass2) + 8u)
  SuperClass (0xb707c3c0) 0 nearly-empty
      primary-for SubClass2 (0xb7254b80)

Vtable for SubClass3
SubClass3::_ZTV9SubClass3: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI9SubClass3)
8     SuperClass::funcA
12    SubClass3::funcB

Class SubClass3
   size=4 align=4
   base size=4 base align=4
SubClass3 (0xb7254c40) 0 nearly-empty
    vptr=((& SubClass3::_ZTV9SubClass3) + 8u)
  SuperClass (0xb707c528) 0 nearly-empty
      primary-for SubClass3 (0xb7254c40)

おわり

Related posts:

  1. C++ :: typeidとdemangleで実行時クラス名取得

Posted in: Programming / プログラミング

Tags: , , , ,



DeliciousFacebookRedditTwitterGoogle

addLeave a comment