スコルの知恵袋

主にプログラミング関係の気になったこと等をまとめたブログ

コンストラクタから呼び出す仮想関数には気を付けよう【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、ということらしい。

まとめ

コンストラクタから仮想関数を呼び出すとオーバーライド前の動作をしてしまう。気を付けよう!