c++のvirtualのデメリット

c++では一般的にvirtualを付けておくとメリットがあるという場合が多い。
例えばこのような場合

#include <iostream>
using namespace std;

class A
{
    int m_a;
public:
    A() { cout << "コンストラクタA" << endl; };
    ~A() { cout << "デストラクタA" << endl; }; # 

};

class B : public A
{
    int m_b;
public:
    B() { cout << "コンストラクタB" << endl; };
    ~B() { cout << "デストラクタB" << endl; }; // これが呼ばれない
};

int main()
{
    A* obj = new B();

    delete obj;

    return 0;
}

このプログラムを実行すると、deleteで解放する際にクラスBのデストラクタ~B()が呼ばれない。
その為~B()内でメモリの解放等の処理を行う場合にはメモリが正常に解放されないという問題がある。
これの問題はクラスAのデストラクタ~A()にvirtualを指定する事で解決できる。
それなら、最初から既定クラスであるAにとりあえずvirtualを付けとけば良いのではないかと思いませんか?


確かにその通りで~A()にvirutualを指定しておけばAのインスタンスを作成する場合にも、
Bのインスタンスを作成する場合にも正常に動きます。
でも、virtualを付けるとデメリットもあります。


例えばこれ

#include <iostream>
using namespace std;

class A1
{
    int m_a;
public:
    A1() { cout << "コンストラクタA1" << endl; };
    ~A1() { cout << "デストラクタA1" << endl; };

    void func1() { cout << "A1のfunc1" << endl; };
    void func2() { cout << "A1のfunc2" << endl; };
};

class A2
{
    int m_a;
public:
    A2() { cout << "コンストラクタA2" << endl; };
    virtual ~A2() { cout << "デストラクタA2" << endl; };

    virtual void func1() {cout << "A2のfunc1" << endl;};
    void func2() {cout << "A2のfunc2" << endl;};
};

int main()
{
    cout << "A1のサイズ" << sizeof(A1) << "byte" << endl;
    cout << "A2のサイズ" << sizeof(A2) << "byte" << endl;

    return 0;
}

このプログラムを実行すると以下のような出力になります。

A1のサイズ4byte
A2のサイズ8byte

virtualをつけた場合の方付けない方のクラスと比較して4バイト多いんです。
何故4バイト多いのかというと、これは仮想関数テーブル(vtable)とういうのが関係しています。


vtableはvirtual宣言されている関数のアドレスを集めた関数ポインタの配列です。
クラス内にvirtualを付けたメンバ関数がある場合にコンパイラはそのクラス単位でvtableというのを作成します。
そしてインスタンスが作成されると生成されたインスタンス毎に隠しメンバとしてvtableへのポインタが付加されます。
このポインタのサイズは32bit環境では4byte,64bit環境では8byteになります。


g++では-fdump-class-hierarchyのオプションを指定してコンパイルする事で仮想関数テーブルを出力する事ができます。
ダンプファイルはソースファイル名に .class を追加して作成されます。
出力結果を見ると以下のようになります。

Class A1
   size=4 align=4
   base size=4 base align=4
A1 (0xb6a63a14) 0

Vtable for A2
A2::_ZTV2A2: 5u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI2A2)
8     A2::~A2
12    A2::~A2
16    A2::func1

Class A2
   size=8 align=4
   base size=8 base align=4
A2 (0xb6a63a50) 0
    vptr=((& A2::_ZTV2A2) + 8u)

A2のVtableというのが作られていて、
Class A2の最後にvptrというのが付加されている事が確認できます。


つまり、virtualを付加した場合には仮想関数テーブルのサイズに加え、
各々のインスタンスに付加される仮想関数ポインタのサイズ分メモリを食うという事になります。