C++では、動的ポリモーフィズムは、特別なテーブルであるvtable(仮想テーブル)を使用した仮想関数のメカニズムを通じて実現されます。少なくとも1つの仮想関数を持つクラスに対して、コンパイラーはvtableを生成します: これは、そのクラスの仮想関数へのポインタの配列です。このようなクラスの各オブジェクトは、vtableへの隠れたポインタであるvptr(仮想ポインタ)を保持します。
オブジェクトの作成時に、仮想関数を持つクラスのvptrは、そのオブジェクトのクラスに対応するvtableに初期化されます。仮想関数を呼び出す際、呼び出しは直接行われるのではなく、vtableに格納されたアドレスを介して行われ、実際のオブジェクトの型に応じた実装が選択されます(ベース型を介してアクセスする場合でも)。
class Base { public: virtual void foo() { std::cout << "Base::foo() "; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo() "; } }; void call_foo(Base* obj) { obj->foo(); // vtableを介しての呼び出し }
呼び出しfooはvtableへのアクセスとなります: Derivedの場合、オーバーライドされた関数が呼び出されます。
"メソッドが
virtualとして宣言されていても、派生クラスでオーバーライドされていない場合、それは仮想的と見なされますか?この場合、vtableを介しての呼び出しは機能しますか?"
答え: はい、関数が基底クラスでvirtualとして宣言されている場合、オーバーライドされていなくてもすべての子クラスに対して依然として仮想的です。基底ポインタを介した呼び出しは、いずれにせよvtableを介して行われます。メソッドがオーバーライドされていない場合、基底クラスのバージョンが呼び出されます。
例:
struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // "B"が出力されますが、vtableを介しての呼び出しです!
ストーリー
大規模プロジェクトで、仮想関数を持つ基底クラスでデストラクタを仮想として宣言し忘れました。これによりメモリリークが発生し、基底ポインタを介して削除する際に、派生クラスのデストラクタではなく、基底クラスのデストラクタのみが呼び出されました。
ストーリー
クラスには仮想メソッドが宣言されていましたが、間違って派生クラスで隠されていました(
overrideではなく、異なるシグネチャ/引数名で)。基底クラスを介した呼び出しが必要な関数に届かず、不正な動作を引き起こしました。
ストーリー
複数継承時のvtableの更新がエラーを引き起こしました: dynamic_castを使用した場合にメソッドの不正な呼び出しが発生しました。これは、複雑なクラス階層におけるvptrのオフセットが考慮されなかったためです。