コンストラクタから呼び出す仮想関数には気を付けよう【C++】
これは、研究のためのコードを書いているときに遭遇したことを軽くまとめたメモ。
仮想関数とオーバーライド
まず、仮想関数とオーバーライドのおさらい。
C++ではメンバ関数にvirtual
指定子をつけるとそれは仮想関数になる。仮想関数にすることで派生クラスでオーバーライドできるようになる。
基底クラスのメンバ関数を派生クラスでオーバーライドすると、派生クラスのオブジェクトからその関数を呼び出したときの動作を再定義することができる。
例:
class Inu { public: void say() { std::cout << getName() << std::endl; } virtual const char* getName() { return "Inu"; } }; class Pomeranian : public Inu { public: const char* getName() { return "Pomeranian"; } }; int main() { Pomeranian pome; pome.say(); }
出力:
Pomeranian
getName()
は基底クラスInu
のメンバ関数say()
で呼び出されているのにもかかわらず、その動作はInu
ではなく派生クラスPomeranian
で定義されたものとなっているのがポイント。
getName()
にvirtual
をつけなければ出力は
Inu
となる。virtual
をつけなければ、メンバ関数を派生クラスで再定義しても、それを基底クラスのメンバ関数から呼び出してしまうと基底クラスで定義された動作をしてしまう。
本題
本題に入る。次のようなコードを考える。
class Inu { public: Inu() { std::cout << getName() << std::endl; } virtual const char* getName() { return "Inu"; } }; class Pomeranian : public Inu { public: const char* getName() { return "Pomeranian"; } }; int main() { Pomeranian pome; }
前との違いは、Inu
のコンストラクタの中で出力するようにしているところのみ。
前の結果を踏まえるとこの出力は
Pomeranian
になりそうだな~と思ってしまうが、実際は
Inu
となる。はぇ~~
でも、よく考えればInu
のコンストラクタが動いているときは、まだPomeranian
の初期化が完了していないわけだから、このような挙動になるのは「そっか~~」という感じだ。
ちゃんと調べると、仮想関数のオーバーライドは「『仮想関数テーブル』という隠しメンバ変数を上書きする」という形で行われるらしく、この操作は派生クラスのコンストラクタで行われるので、 その前に仮想関数を呼び出すとオーバーライド前の動作となってしまう*1、ということらしい。
まとめ
コンストラクタから仮想関数を呼び出すとオーバーライド前の動作をしてしまう。気を付けよう!