More Effective C++ 項目24 仮想関数、多重継承、仮想基底クラスと実行時型識別のコストを理解する
新訂版 More Effective C++ (AddisonーWesley professional co)
- 作者: スコット・メイヤーズ,安村通晃,伊賀聡一郎,飯田朱美,永田周一
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2007/06/29
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 129回
- この商品を含むブログ (43件) を見る
項目24 仮想関数、多重継承、仮想基底クラスと実行時型識別のコストを理解する
仮想関数が呼ばれるとき、実行されるコードはその関数を呼び出したオブジェクトの動的な型に対応している。
コンパイラはこの処理を実現するために、仮想テーブル(vtbl)と仮想テーブルポインタ(vptr)を使う。
vtblは関数へのポインタの配列で、仮想関数を宣言したり継承したりしているクラスはそれぞれvtblを持っている。
例えば以下のようなクラス定義の場合、
class C1 { public: C1(); virtual ~C1(); virtual void f1(); virtual int f2(char c) const; virtual void f3(const string& s); void f4() const; ... };
C1のvtblは次のようになる。
コンストラクタやvirtualでないf4()は、一般的なCの関数と同じように実現されるため、vtblには含まれない。
C1を継承していくつかの仮想関数をオーバーライドすると、vtblは適切な関数を指し示すようになる。
class C2 : public C1 { public: C2(); virtual ~C2(); virtual void f1(); virtual void f5(char* str); ... };
C2が再定義しなかったC1の仮想関数へのポインタも含まれる。
仮想関数のコスト
仮想関数を含んでいるクラスは、仮想テーブルvtblのスペースが必要になり、その大きさは、仮想関数の数に比例する。(関数ポインタの配列)
仮想関数を含む各クラスにvtblが必要だが、個々のオブジェクトには仮想テーブルポインタ(vptr)が必要となる。ポインタ1つ分だが、数バイトのオブジェクトの場合は結構でかい。
(コンパイラによって仮想ポインタを置く場所は異なり、オブジェクトの末尾とは限らない)
仮想関数の呼び出しコスト
void makeACall(C1* pC1)
{
pC1->f1();
}
このようにf1()を呼び出すと、コンパイラは以下を実行するコードを生成する。
オブジェクトの仮想ポインタを追って仮想テーブルにアクセス
呼ばれている関数に対応したポインタを仮想テーブルから見つける
2.で見つけた関数を呼ぶ
関数f1の仮想テーブル上の添字がiの場合、
pC1->f();
と書くと以下のようなコードが生成させる。
(*pC1->vptr[i])(); // pC1->vptrが指す仮想テーブルのi番目の関数を呼び出す
関数ポインタの配列をたどるだけなので、非仮想関数呼び出しと比べても呼び出しコストはそれほど変わらない。
多重継承
単一継承と比べて、オブジェクト内部で仮想ポインタを見つけるためのオフセットの計算が複雑になる。
1つの基底クラスごとに仮想ポインタが必要となり、クラスごと、オブジェクトごとの仮想関数のためのスペースが大きくなり、呼び出しコストもわずかに増加する。
実行時型識別
1つのクラスにつきtype_info型オブジェクトが必要になる。
仮想関数テーブルにtype_infoオブジェクトが追加されるイメージで、仮想関数をたどるようにクラス情報を取れる。