C++入門|仮想デストラクタ

親クラス型で子クラスを扱うなら、退場処理まで子クラスに届けよう。仮想デストラクタは、継承したオブジェクトを安全に片付けるための大切な守り札です。

C++では、親クラス型のポインタを使って、子クラスのインスタンスを指すことができます。

たとえば、基本戦士を表す親クラスがあり、その子クラスとして亀仙流戦士を作ったとします。

BaseWarrior* pWarrior = new KameWarrior();

このように書くと、ポインタの型はBaseWarriorですが、実際に生成されているインスタンスはKameWarriorです。

これは、ポリモーフィズムを使うときによく出てくる形です。
親クラス型のポインタで、いろいろな子クラスのインスタンスをまとめて扱えるので、とても便利です。

ただし、ここで大切になるのがデストラクタです。

newで生成したインスタンスは、使い終わったらdeleteで消去します。

delete pWarrior;

このとき、pWarriorの型はBaseWarrior*です。
でも、実際のインスタンスはKameWarriorです。

では、deleteしたときに、BaseWarriorのデストラクタだけが呼ばれるのでしょうか。
それとも、KameWarriorのデストラクタも正しく呼ばれるのでしょうか。

この問題を安全に解決するために使うのが、仮想デストラクタです。

親クラスのデストラクタにvirtualを付けると、親クラス型のポインタからdeleteしても、実際の子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。

ドラゴンボール風にたとえるなら、KameWarriorが特別な修行装備を身につけていた場合、退場するときにはまずその装備を外し、そのあと基本戦士としての共通処理を終える必要があります。

親クラスのデストラクタがvirtualでないと、子クラス側の装備解除が正しく行われない可能性があります。

状態delete時の動きイメージ
親クラスのデストラクタにvirtualなし子クラス側のデストラクタが正しく呼ばれない危険がある特別装備の解除が飛ばされる
親クラスのデストラクタにvirtualあり子クラス → 親クラスの順にデストラクタが呼ばれる特別装備を外してから共通処理を終える

継承して使われる可能性のある親クラスでは、デストラクタにvirtualを付けることがとても大切です。

図:親クラス型ポインタで子クラスをdeleteする場面

この図が示していること

この図では、BaseWarrior型のポインタが、実際にはKameWarriorのインスタンスを指している様子を表しています。

この状態でdelete pWarriorを行うとき、KameWarrior側のデストラクタまで正しく呼ぶ必要があります。

親クラスのデストラクタにvirtualが付いていれば、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。

仮想デストラクタとは

仮想デストラクタとは、virtual修飾子を付けたデストラクタのことです。

virtual ~BaseWarrior();

通常の仮想関数と同じように、デストラクタにもvirtualを付けられます。

親クラスのデストラクタにvirtualを付けると、親クラス型のポインタで子クラスのインスタンスをdeleteした場合でも、実際の子クラスに合わせてデストラクタが呼び出されます。

たとえば、次のような形です。

BaseWarrior* pWarrior = new KameWarrior();
delete pWarrior;

このとき、BaseWarriorのデストラクタがvirtualなら、次の順番でデストラクタが呼ばれます。

KameWarriorのデストラクタ
BaseWarriorのデストラクタ

これは、子クラスの部分を先に片付け、そのあと親クラスの部分を片付けるという自然な流れです。

ドラゴンボール風に言えば、亀仙流戦士としての専用装備や技のエネルギーを先に解除し、そのあと基本戦士としての共通状態を終了するイメージです。

仮想デストラクタなしのクラス

まずは、親クラスのデストラクタにvirtualを付けない例を見てみます。

この例では、BaseWarriorPlainという親クラスと、それを継承したKameWarriorPlainという子クラスを作ります。

BaseWarriorPlainのデストラクタにはvirtualを付けません。

プロジェクト/ファイル名: Chap5_08/base_warrior_plain.h

#ifndef _BASE_WARRIOR_PLAIN_H_
#define _BASE_WARRIOR_PLAIN_H_

#include <iostream>

class BaseWarriorPlain {
public:
    // コンストラクタ
    BaseWarriorPlain() {
        std::cout << "BaseWarriorPlainのコンストラクタ" << std::endl;
    }

    // virtualなしのデストラクタ
    ~BaseWarriorPlain() {
        std::cout << "BaseWarriorPlainのデストラクタ" << std::endl;
    }
};

#endif // _BASE_WARRIOR_PLAIN_H_

このクラスでは、コンストラクタとデストラクタの中でメッセージを表示しています。

注目するのは、デストラクタです。

~BaseWarriorPlain() {
    std::cout << "BaseWarriorPlainのデストラクタ" << std::endl;
}

virtualが付いていません。

つまり、BaseWarriorPlain型のポインタで子クラスをdeleteしたとき、子クラス側のデストラクタ呼び出しが保証されません。

仮想デストラクタありのクラス

次に、デストラクタにvirtualを付けた親クラスを作ります。

プロジェクト/ファイル名: Chap5_08/base_warrior_virtual.h

#ifndef _BASE_WARRIOR_VIRTUAL_H_
#define _BASE_WARRIOR_VIRTUAL_H_

#include <iostream>

class BaseWarriorVirtual {
public:
    // コンストラクタ
    BaseWarriorVirtual() {
        std::cout << "BaseWarriorVirtualのコンストラクタ" << std::endl;
    }

    // virtualありのデストラクタ
    virtual ~BaseWarriorVirtual() {
        std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
    }
};

#endif // _BASE_WARRIOR_VIRTUAL_H_

こちらのデストラクタには、virtualが付いています。

virtual ~BaseWarriorVirtual() {
    std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
}

このようにしておくと、BaseWarriorVirtual型のポインタで子クラスのインスタンスをdeleteした場合でも、子クラス側のデストラクタが正しく呼ばれます。

virtualなしの子クラス

次に、BaseWarriorPlainを継承したKameWarriorPlainを作ります。

プロジェクト/ファイル名: Chap5_08/kame_warrior_plain.h

#ifndef _KAME_WARRIOR_PLAIN_H_
#define _KAME_WARRIOR_PLAIN_H_

#include "base_warrior_plain.h"
#include <iostream>

class KameWarriorPlain : public BaseWarriorPlain {
public:
    // コンストラクタ
    KameWarriorPlain() {
        std::cout << "KameWarriorPlainのコンストラクタ" << std::endl;
    }

    // デストラクタ
    ~KameWarriorPlain() {
        std::cout << "KameWarriorPlainのデストラクタ" << std::endl;
    }
};

#endif // _KAME_WARRIOR_PLAIN_H_

KameWarriorPlainは、BaseWarriorPlainを継承しています。

class KameWarriorPlain : public BaseWarriorPlain

コンストラクタでは、KameWarriorPlainの生成メッセージを表示します。

デストラクタでは、KameWarriorPlainの消去メッセージを表示します。

~KameWarriorPlain() {
    std::cout << "KameWarriorPlainのデストラクタ" << std::endl;
}

しかし、親クラスBaseWarriorPlainのデストラクタにvirtualが付いていないため、BaseWarriorPlain型のポインタからdeleteすると、KameWarriorPlainのデストラクタが呼ばれない危険があります。

virtualありの子クラス

次に、BaseWarriorVirtualを継承したKameWarriorVirtualを作ります。

プロジェクト/ファイル名: Chap5_08/kame_warrior_virtual.h

#ifndef _KAME_WARRIOR_VIRTUAL_H_
#define _KAME_WARRIOR_VIRTUAL_H_

#include "base_warrior_virtual.h"
#include <iostream>

class KameWarriorVirtual : public BaseWarriorVirtual {
public:
    // コンストラクタ
    KameWarriorVirtual() {
        std::cout << "KameWarriorVirtualのコンストラクタ" << std::endl;
    }

    // デストラクタ
    ~KameWarriorVirtual() {
        std::cout << "KameWarriorVirtualのデストラクタ" << std::endl;
    }
};

#endif // _KAME_WARRIOR_VIRTUAL_H_

KameWarriorVirtualは、BaseWarriorVirtualを継承しています。

BaseWarriorVirtualのデストラクタにはvirtualが付いているため、BaseWarriorVirtual型のポインタからdeleteしても、KameWarriorVirtualのデストラクタが先に呼ばれます。

そのあと、BaseWarriorVirtualのデストラクタが呼ばれます。

main.cppで動きを確認する

最後に、main.cppを作ります。

ここでは、virtualなしの場合と、virtualありの場合を並べて確認します。

プロジェクト/ファイル名: Chap5_08/main.cpp

#include <iostream>
#include "base_warrior_plain.h"
#include "base_warrior_virtual.h"
#include "kame_warrior_plain.h"
#include "kame_warrior_virtual.h"

using namespace std;

int main(int argc, char** argv) {
    cout << "*** 仮想デストラクタなし ***" << endl;

    // 親クラス型のポインタで子クラスのインスタンスを指す
    BaseWarriorPlain* pPlain = new KameWarriorPlain();

    // 親クラス型のポインタからdeleteする
    // 親クラスのデストラクタにvirtualがないため、子クラス側の後片付けが保証されない
    delete pPlain;

    cout << "*** 仮想デストラクタあり ***" << endl;

    // 親クラス型のポインタで子クラスのインスタンスを指す
    BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();

    // 親クラス型のポインタからdeleteする
    // 親クラスのデストラクタにvirtualがあるため、子クラス側から順に後片付けされる
    delete pVirtual;

    return 0;
}

実行結果の例

*** 仮想デストラクタなし ***
BaseWarriorPlainのコンストラクタ
KameWarriorPlainのコンストラクタ
BaseWarriorPlainのデストラクタ
*** 仮想デストラクタあり ***
BaseWarriorVirtualのコンストラクタ
KameWarriorVirtualのコンストラクタ
KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタ

仮想デストラクタなしの部分では、KameWarriorPlainのコンストラクタは呼ばれています。
しかし、delete時にはBaseWarriorPlainのデストラクタだけが表示されています。

一方、仮想デストラクタありの部分では、delete時にKameWarriorVirtualのデストラクタが先に呼ばれ、そのあとBaseWarriorVirtualのデストラクタが呼ばれています。

C++の厳密なルールでは、親クラスのデストラクタがvirtualでない状態で、親クラス型ポインタから子クラスのインスタンスをdeleteするのは安全ではありません。
そのため、継承して使う親クラスでは、デストラクタをvirtualにするのが基本です。

図:virtualなしとvirtualありのdelete時の違い

この図が示していること

この図では、virtualなしとvirtualありで、delete時のデストラクタ呼び出しがどう変わるかを表しています。

virtualなしでは、親クラス型ポインタからdeleteしたときに、子クラス側のデストラクタが正しく呼ばれない危険があります。

virtualありでは、実際のインスタンスが子クラスであることに合わせて、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれます。

生成時のコンストラクタの順番

まず、newしたときの動きを確認しましょう。

BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();

このように、子クラスのインスタンスを生成すると、コンストラクタは親クラスから子クラスの順番で呼ばれます。

BaseWarriorVirtualのコンストラクタ
KameWarriorVirtualのコンストラクタ

これは、子クラスの中には親クラス部分が含まれているからです。

KameWarriorVirtualという戦士を作るには、まずBaseWarriorVirtualとしての基本部分を作り、そのあとKameWarriorVirtualとしての専用部分を作ります。

タイミング呼び出し順
生成時親クラスのコンストラクタ → 子クラスのコンストラクタ
消去時子クラスのデストラクタ → 親クラスのデストラクタ

生成時は親から子へ。
消去時は子から親へ。

この流れを押さえると、仮想デストラクタの意味が分かりやすくなります。

消去時のデストラクタの順番

virtualありの場合、delete時には次の順番になります。

KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタ

これは、作るときと逆の順番です。

ドラゴンボール風に言えば、亀仙流戦士としての専用装備や気の設定を先に解除し、そのあと基本戦士としての共通部分を終了するイメージです。

もし子クラス側のデストラクタが呼ばれないと、専用装備の解除処理が行われません。

プログラムの世界では、それがメモリ解放漏れやリソース解放漏れにつながる可能性があります。

virtualなしが危険な理由

virtualなしの場合、次のような形が危険です。

BaseWarriorPlain* pPlain = new KameWarriorPlain();
delete pPlain;

pPlainの型はBaseWarriorPlain*です。
しかし、実際に生成されているのはKameWarriorPlainです。

親クラスのデストラクタにvirtualが付いていないと、delete時に子クラスのデストラクタが正しく呼ばれることが保証されません。

そのため、KameWarriorPlain側で大切な後片付けをしていた場合、その処理が実行されない危険があります。

たとえば、子クラス側で次のようなものを管理している場合です。

子クラスが持つものデストラクタで必要な処理
動的確保したメモリdeleteやdelete[]で解放する
ファイルcloseする
通信接続接続を閉じる
修行装備の管理情報終了処理を行う

子クラスのデストラクタが呼ばれないと、これらの後片付けが抜けてしまう可能性があります。

C++では、このような状態は非常に危険です。

virtualありなら子クラス側から安全に片付く

virtualありの場合は、次のように書いても安全に片付けられます。

BaseWarriorVirtual* pVirtual = new KameWarriorVirtual();
delete pVirtual;

BaseWarriorVirtualのデストラクタにはvirtualが付いています。

virtual ~BaseWarriorVirtual() {
    std::cout << "BaseWarriorVirtualのデストラクタ" << std::endl;
}

そのため、delete pVirtualを実行すると、実際のインスタンスであるKameWarriorVirtualのデストラクタが呼ばれます。

そのあと、BaseWarriorVirtualのデストラクタが呼ばれます。

つまり、次の順番になります。

KameWarriorVirtualのデストラクタ
BaseWarriorVirtualのデストラクタ

これなら、子クラスが持つ専用の後片付けを先に行い、そのあと親クラスの共通部分を片付けられます。

親クラス側にvirtualを付けることが大切

仮想デストラクタで大切なのは、親クラスのデストラクタにvirtualを付けることです。

子クラス側のデストラクタにだけ気をつけるのではありません。

親クラス型のポインタからdeleteする可能性があるなら、親クラスのデストラクタをvirtualにします。

class BaseWarrior {
public:
    virtual ~BaseWarrior();
};

このようにしておくと、BaseWarrior*で子クラスを指している場合でも、delete時に正しいデストラクタの連鎖が起きます。

クラスデストラクタの書き方安全性
親クラス~BaseWarrior();親クラス型ポインタでdeleteすると危険
親クラスvirtual ~BaseWarrior();子クラス側まで正しく片付けられる
子クラス~KameWarrior();親がvirtualなら、このデストラクタも適切に呼ばれる

親クラスがvirtualにしておくことで、派生先のクラスも安全に扱いやすくなります。

仮想デストラクタが必要になる典型例

仮想デストラクタが特に大切になるのは、親クラス型のポインタや参照で子クラスを扱う場面です。

BaseWarrior* warriors[2];

warriors[0] = new KameWarriorVirtual();
warriors[1] = new KameWarriorVirtual();

このように、配列やリストで複数の子クラスを親クラス型としてまとめることがあります。

そのあと、次のようにdeleteすることもあります。

for (int i = 0; i < 2; i++) {
    delete warriors[i];
}

このとき、BaseWarriorのデストラクタがvirtualでないと、子クラス側のデストラクタが呼ばれない危険があります。

ポリモーフィズムを使う設計では、親クラス型ポインタで子クラスをdeleteする場面が自然に出てきます。
だからこそ、継承される親クラスのデストラクタにはvirtualを付けます。

図:子クラスの後片付けが飛ばされると危険

この図が示していること

この図では、子クラス側のデストラクタが呼ばれないと、子クラス専用の後片付けが飛ばされる危険があることを表しています。

virtualなしでは、親クラス型ポインタからdeleteしたときに、子クラスの片付けが正しく行われない可能性があります。

virtualありでは、子クラスのデストラクタが先に呼ばれ、そのあと親クラスのデストラクタが呼ばれるため、安全に後片付けできます。

仮想関数と仮想デストラクタの関係

virtualは、通常のメンバ関数だけでなく、デストラクタにも付けられます。

通常の仮想関数は、親クラス型ポインタから呼び出したときに、実際の子クラスの関数を呼び出すために使います。

仮想デストラクタも考え方は似ています。

親クラス型ポインタからdeleteしたときに、実際の子クラスのデストラクタを呼び出すために使います。

種類目的
virtual void shout();親クラス型ポインタから子クラス側の関数を呼ぶ
virtual ~BaseWarrior();親クラス型ポインタから子クラス側のデストラクタを呼ぶ

どちらも、「ポインタの型」ではなく「実際のインスタンス」に合わせて処理を行うための仕組みです。

デストラクタは消去時の最後の安全装置

デストラクタは、インスタンスが消えるときに自動的に呼び出される関数です。

子クラスのデストラクタには、子クラス専用の後片付けを書くことがあります。

たとえば、KameWarriorが専用の修行データを動的に確保している場合、その解放処理はKameWarriorのデストラクタに書くことになります。

もしKameWarriorのデストラクタが呼ばれなければ、その解放処理も実行されません。

そのため、継承を使う設計では、デストラクタをただの終了メッセージとして考えないことが大切です。

デストラクタは、メモリやリソースを安全に片付けるための最後の安全装置です。

仮想デストラクタで押さえたいポイント

仮想デストラクタでは、次のポイントを押さえておきましょう。

ポイント内容
仮想デストラクタvirtualを付けたデストラクタ
必要な場面親クラス型ポインタで子クラスをdeleteする場面
virtualなし子クラスのデストラクタが正しく呼ばれない危険がある
virtualあり子クラス → 親クラスの順にデストラクタが呼ばれる
生成時の順番親クラスのコンストラクタ → 子クラスのコンストラクタ
消去時の順番子クラスのデストラクタ → 親クラスのデストラクタ
基本方針継承して使う親クラスのデストラクタにはvirtualを付ける

C++では、親クラス型のポインタで子クラスのインスタンスを扱うことがよくあります。

そのとき、deleteまで安全に行うには、親クラスのデストラクタをvirtualにしておく必要があります。

ドラゴンボール風に言えば、BaseWarrior型の司令端末からKameWarriorを退場させるときでも、亀仙流戦士としての専用装備や気の設定をきちんと解除してから、基本戦士としての共通処理を終える必要があります。

その順番を守らせるのが、仮想デストラクタです。

継承される可能性のある親クラスでは、次の形を基本として覚えておきましょう。

virtual ~BaseWarrior();

この1行があることで、ポリモーフィズムを使ったクラス設計を安全に扱いやすくなります。