C++のコンストラクター内で仮想関数を呼び出すとどうなるか?危険性と理由を解説
C++では、コンストラクターやデストラクター内から仮想関数を呼び出すことは危険であり、可能な限り避けるべきとされています。
その理由は、コンストラクター内で呼び出した仮想関数が派生クラスのものではなく、常に基底クラス(Baseクラス)のものとして実行されるためです。直感的には「派生クラスでオーバーライドした関数が呼ばれる」と思いがちですが、実際の動作は異なります。
なぜ基底クラス側の仮想関数が呼ばれるのか?
C++では、スーパークラス(基底クラス)が派生クラスより先に構築されるという規則があります。つまり、派生クラスのオブジェクトを生成する場合、処理順序は次のようになります。
- まず基底クラスのコンストラクターが実行される
- その後で派生クラスのコンストラクターが実行される
以下の例で考えてみましょう。クラス D をインスタンス化するには、その前に必ず基底クラス B が先にインスタンス化されている必要があります。B のコンストラクターが呼び出された時点では、オブジェクトはまだ「D」として完成していないため、仮想関数テーブル(vtable)には依然として B 側の s() へのエントリが設定されたままです。
このため、D クラスで s() をオーバーライドしていても、B のコンストラクター内から呼び出されるのは B::s() となり、期待した多態性(ポリモーフィズム)は実現されません。
サンプルコード
#include<iostream>
using namespace std;
class B {
public:
B() {
s(); // コンストラクター内で仮想関数を呼び出す
}
virtual void s() {
cout << "Base" << endl;
}
};
class D : public B {
public:
D() : B() {}
virtual void s() {
cout << "Derived" << endl;
}
};
int main() {
D de;
}
実行結果
Base
D 型のオブジェクトを生成しているにもかかわらず、出力されるのは D ではなく Base です。これは、B のコンストラクター実行時点ではまだ D として初期化されていないため、D::s() ではなく B::s() が呼び出されたことを示しています。
さらに注意すべきポイント:純粋仮想関数の場合
もしコンストラクター内から純粋仮想関数を直接呼び出した場合、未定義動作(undefined behavior)となり、多くの環境ではプログラムの異常終了やランタイムエラー(例:「pure virtual method called」)が発生します。純粋仮想関数には基底クラス時点で実体がないためです。
回避策・ベストプラクティス
- コンストラクターやデストラクター内で仮想関数を呼ばない設計にする
- 初期化が必要な場合は、オブジェクト生成後に明示的に
init()などの通常メソッドを呼び出す(Two-phase initialization パターン) - ファクトリー関数を使い、オブジェクト生成後に必要な処理を実行する
C++のオブジェクトは段階的に構築されるという特性を理解し、コンストラクター内での仮想関数呼び出しは避けることで、意図しないバグを防ぐことができます。
-
C++のコンストラクタとは?デフォルトコンストラクタと引数付きコンストラクタの使い方を解説
C++のコンストラクタとはコンストラクタ(constructor)とは、クラスの新しいオブジェクトが生成される瞬間に自動的に呼び出される特別なメンバ関数です。コンストラクタには以下のような特徴があります。関数名がクラス名と同じである戻り値の型を持たない(voidも指定しない)オブジェクト生成時に自動的に実行されるコンストラクタの主な役割は、クラスのメンバ変数に初期値を与えることです。これにより、オブジェクトは生成された時点で適切な状態を持つことが保証されます。C++のコンストラクタには、主に「デフォルトコンストラクタ」と「引数付きコンストラクタ(パラメータ化コンストラクタ)」の2種類があります
-
C++のコンストラクタ内で仮想関数を呼び出すとどうなる?仕組みと注意点を解説
コンストラクタやデストラクタの中から仮想関数を呼び出すのは危険な操作であり、可能な限り避けるべきです。その理由は、呼び出された仮想関数が派生クラスのものではなく、基底クラス(Base)のものとして実行されてしまうためです。 なぜ基底クラスの仮想関数が呼ばれるのか C++では、各クラスは自分自身の構築処理に入る前に、そのクラス専用の仮想メソッドテーブル(vtable)を先に構築します。この仕組みにより、コンストラクタ内で仮想メソッドを呼び出すと、現時点で有効になっているテーブル、つまり基底クラスの仮想メソッドが呼び出されることになります。 また、その階層に実装がまったく存在しない場合(純粋仮想関