Seaside Laboratory

Posts

pure virtual function call の対処法

C++ でコードを書いていると、以下のようなエラーでクラッシュしてしまうことがある。

R6025 - pure virtual function call

たまにしか遭遇しないので何が問題なのか度忘れしていることが多く、その度に調べ直すのが面倒なので対処法をまとめておくことにした。

原因

オブジェクト指向プログラミングでは「基底クラスに共通の処理を定義して派生クラスでカスタマイズする」ということをよく行うが、以下のようなコードを書いてしまうと pure virtual function call が発生する。

// ピザ
class CPizza
{
public:
    CPizza()
    {
        // 生成されたピザの名前を表示
        std::puts( this->GetName() );
    }

    // 名前は派生先で定義する
    virtual const char* GetName() const = 0;
};

// マルゲリータ
class CMargherita : public CPizza
{
public:
    const char* GetName() const override
    {
        return "Margherita";
    }
};

int main()
{
    // ピザを作る
    auto lpMargherita = new CMargherita();

    delete lpMargherita;

    return 0;
}

派生クラスで基底クラスの機能を利用するには、先に基底クラスを初期化しておく必要があるため、C++ では基底クラスのコンストラクタが呼ばれた後に派生クラスのコンストラクタが呼ばれるようになっている。基底クラスのコンストラクタが呼ばれた時点では派生クラスのオブジェクトが生成されていないため、実体が派生クラス側にある仮想関数を呼び出すとエラーになってしまう。

ちなみに、上記コードのようにコンストラクタから直接仮想関数を呼び出しているケースでは、コンパイル時にリンカが検出してくれるので実行時エラーになることはない。

対策

先程のコード例を元に対策方法を書いていく。

初期化処理をコンストラクタ外に追い出す

まず、基底クラスのコンストラクタ内にある初期化処理を Initialize 関数として切り出す。

class CPizza
{
public:
    CPizza()
    {
        // 何もしない
    }

    void Initialize()
    {
        // 生成されたピザの名前を表示
        std::puts( this->GetName() );
    }

    virtual const char* GetName() const = 0;
};

そして、派生クラスのオブジェクトを生成した後に Initialize 関数を呼び出す。

int main()
{
    // ピザを作る
    auto lpMargherita = new CMargherita();
    // 初期化
    lpMargherita->Initialize();

    delete lpMargherita;

    return 0;
}

オブジェクトは生成済みなので仮想関数を安全に呼び出すことができる。

欠点は明示的に Initialize 関数を呼び出さなければいけないことで、クラス利用者に余計な負担を強いることになる。

コンストラクタ経由で派生クラスの情報を渡す

まず、派生クラス固有の情報を基底クラスのコンストラクタに渡すようにする。

class CMargherita : public CPizza
{
public:
    CMargherita() :
        CPizza( "Margherita" ) // 基底クラスにピザの名前を渡す
    {
    }
};

基底クラスは派生クラスから受け取った情報をメンバ変数に保存する。

class CPizza
{
public:
    CPizza( const char* lpszName ) :
        m_lpszName( lpszName ) // 渡されたピザの名前を保存
    {
        std::puts( this->GetName() );
    }
    
    const char* GetName() const
    {
        return this->m_lpszName;
    }

private:
    const char* const m_lpszName;
};

処理が基底クラス内で完結するようになり、問題なく動作させることができる。

ただ、基底クラスのコンストラクタにデータを渡すだけで済んでしまうようなケースでは、わざわざ派生クラスを作る必要がなく、クラス設計そのものを見直す必要が出てくる。

大抵の場合は派生クラスにそれなりの処理があるので、基底クラスに必要最小限のデータだけを渡してコンストラクタをやり過ごす、という作りに落ち着くことになる。