C++の仮想メンバ関数。主に仮想デストラクタ。

先日、継承の階層が3層(親・子・孫)となっている場合、子のクラスのデストラクタにvirtualは必要なのか気になったので実験してみました。
ついでにデストラクタ以外の普通のメンバ関数についても検証。
まず以下のようなクラスとmain関数を用意。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    virtual ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    virtual void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

出力結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

sample_baseのポインタでdeleteした場合は当然として、sample_drived1のポインタでdeleteした場合も正常にデストラクトされている。
オブジェクトサイズから見ても仮想関数テーブルがきちんと認識されている事が分かる。
また、デストラクタ同様にechoメンバ関数についてもオブジェクト本来の型の物が呼ばれている。


次に上で用意したクラスのvirtualの位置を変更する。
sample_baseのメンバに付いているvirtualをsample_drived1のメンバへ移動する。
以下のようなコードになる。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    virtual ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    virtual void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

このコードの実行結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
わんわん
size: 1
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

仮想デストラクタを持たないsample_baseのポインタでdeleteを行った場合に正常にデストラクトされていない。
C++的には正常だけど、期待される動作とは違うことは明らか。
対してsample_drived1のポインタでdeleteを行った場合には正常にオブジェクト本来の型のデストラクタから順に呼ばれている。
また、sample_baseのポインタが指すオブジェクトのサイズは1だと判断されており、仮想関数テーブルが完全にシカトされている。


次に、デストラクタはsample_baseからvirtualにし、echo()メンバ関数はsample_derived1からvirtualにしてみる。

#include <iostream>

using namespace std;

class sample_base{
    public:
    sample_base(){
        cout << "construct sample_base." << endl;
    }
    virtual ~sample_base(){
        cout << "destruct sample_base." << endl;
    }

    void echo(){
        cout << "わんわん" << endl;
    }
};

class sample_drived1 : public sample_base{
    public:
    sample_drived1(){
        cout << "\tconstruct sample_drived1." << endl;
    }
    ~sample_drived1(){
        cout << "\tdestruct sample_drived1." << endl;
    }

    virtual void echo(){
        cout << "にゃんにゃん" << endl;
    }
};

class sample_drived2 : public sample_drived1{
    public:
    sample_drived2(){
        cout << "\t\tconstruct sample_drived2." << endl;
    }
    ~sample_drived2(){
        cout << "\t\tdestruct sample_drived2." << endl;
    }

    void echo(){
        cout << "もーもー" << endl;
    }
};

int main(){
    
    cout << "-- delete sample_base pointer. --" << endl;
    sample_base *p_base = new sample_drived2();
    p_base->echo();
    cout << "size: " << sizeof(*p_base) << endl;
    delete p_base;

    cout << endl << "-- delete sample_drived1 pointer. --" << endl;
    sample_drived1 *p_d1 = new sample_drived2();
    p_d1->echo();
    cout << "size: " << sizeof(*p_d1) << endl;
    delete p_d1;

    return 0;
}

出力結果は以下のようになる。

-- delete sample_base pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
わんわん
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

-- delete sample_drived1 pointer. --
construct sample_base.
        construct sample_drived1.
                construct sample_drived2.
もーもー
size: 8
                destruct sample_drived2.
        destruct sample_drived1.
destruct sample_base.

この結果から分かることは、仮想メンバ関数の呼び出し時にオブジェクト本来の型のメンバ関数が呼ばれるためには、継承ツリー上で仮想メンバ関数が宣言されているクラス以下のクラス型のポインタから呼び出す必要があることが分かる。
試してないけど参照でも同じ振る舞いをすると思う。むしろ違ったら困る。


結論:
ポリモルフィズム的に扱う事前提に継承されるクラス(A)のメンバ関数(デストラクタ含む)にvirtualを付ければ、継承ツリーで(A)以下のクラスのポインタ・参照から仮想メンバ関数を呼び出す場合、派生クラスにvirtualを付けずともオブジェクト本来の型のメンバ関数が呼び出される。
上記状況で、継承ツリーで(A)より上位のクラスのポインタ・参照から(A)で定義されている仮想メンバ関数を呼び出す場合、同名のメンバ関数が定義されていればそれが、定義されていなければコンパイルエラーかリンクエラーが発生する。


ここまで書いてすげぇ当たり前の事書いてる気がしてきた上に、実はこれ違いますとか言われたらすごい恥ずかしい。