3.4 仮想関数の裏側
「仮想関数」は,オブジェクトの動的な型に応じて,呼び出すべきメンバー関数を切り替える機能です.C++の中では最も重要な機能の1つであり,本格的にC++を使うのであれば,多かれ少なかれ仮想関数にかかわることになりますから,その仕組みを理解しておくことは重要です.
3.4.1 仮想関数テーブル
仮想関数の実現方法は,C++の言語規格では特に規定がありません.しかし,大多数の処理系では,仮想関数の実現には「仮想関数テーブル」という方法を採用しています.
具体例で見ていきましょう.
class A
{
int a;
public:
virtual void f();
virtual void g();
};
class B : public A
{
int b;
public:
virtual void f();
virtual void g();
};
上のようなクラスAとBがある場合,それぞれのクラス型のオブジェクトのメモリ配置は,だいたい図3.4のようになります.
●図3.4 クラス型のオブジェクトのメモリ配置
このように,AクラスやBクラスのオブジェクトは,ソースコード上には現れませんが,仮想関数テーブルへのポインタをどこかに隠し持っています.そして,仮想関数を呼び出す際は,この仮想関数テーブルへのポインタを介して,間接的に関数を呼び出すことになります.仮想関数テーブルへのポインタは,コンストラクタが呼び出されるときに設定されます.
Bクラスのコンストラクタが呼び出される際には,まずAクラスのコンストラクタが先に呼び出されるわけですが,Aクラスのコンストラクタが実行されている間は,たとえそのとき生成しようとしているのがBクラスのオブジェクトであったとしても,Aクラスの仮想関数テーブルを参照します.そして,Bクラスのコンストラクタが呼び出される段階になって,初めてBクラスの仮想関数テーブルが参照されるようになるのです.デストラクタの場合はその逆の手順になります.Bクラスのデストラクタが呼び出されている間は,Bクラスの仮想関数テーブルを参照していますが,そのあと,Aクラスのデストラクタが呼び出される時点では,Aクラスの仮想関数テーブルを参照することになります.そのため,コンストラクタやデストラクタから仮想関数を呼び出すと,期待した動作にならずに悩むことがよくあります.
また,他のクラスを継承して定義したクラスの場合,基底クラスの仮想関数テーブルも必要になるわけです.結果として,実際に呼び出すかどうかにかかわらず,基底クラスのものも含めて,全仮想関数がリンクされてしまいます.仮想関数は,呼び出し時のオーバーヘッドはそれほどでもありませんが,サイズのオーバーヘッドは状況次第ではかなり大きなものになります.ROM容量が厳しい環境の場合,本当に動的な多相性が必要になるのでなければ,仮想関数の使用は慎重に行ったほうがよいでしょう.