C++ においてデストラクタを仮想関数にしないと問題が起こる場合があり、 Qiita 内にもそのことを取り上げた記事はいくつもあります。
これは C++ の落とし穴としてよく知られたものとして頻繁に言及されるのでかえってかえって落とし穴としてハマり難くなっているのではないかとも思うのですが、単にデストラクタを仮想にすればよいというだけではなく言語仕様とコンパイラの両面から何が起きているのか解説を試みることにします。
問題が起こるコード
あらためてどのような問題かをより分かりやすいコードで示してみます。
#include <iostream>
struct base_class {};
struct foo {
foo(void) {
std::cout << "foo constructing" << std::endl;
}
~foo(void) {
std::cout << "foo destructing" << std::endl;
}
};
class derived_class : public base_class {
foo x;
};
int main() {
base_class* p = new derived_class;
delete p;
}
これは言語仕様としては未定義の挙動ということにはなっていますが、主要なコンパイラで実行した結果としはおそらくデストラクタ内で表示しようとするメッセージが出てこないという結果になると思います。
言語仕様では未定義
このような状況について、言語仕様では delete
の項目に記述があります。
delete
の対象となるオブジェクトにおいて動的な型と静的な型が異なる場合は静的な型が仮想デストラクタを持たなければならず、そうでなければ未定義の挙動ということになっています。
このとき p
(が指す先のオブジェクト)の動的な型 (dynamic type) は derived_class
で静的な型 (static type) は base_class
ということになります。
デストラクタ以外
デストラクタでは問題になりましたが、派生クラスのオブジェクトに対して基底クラスのポインタを通じてメンバ関数を呼び出した場合にはどうなるでしょうか。
#include <iostream>
struct base_class {
int a;
void member_function(void) {
std::cout << "base_" << a << std::endl;
}
};
struct derived_class : public base_class {
double b;
void member_function(void) {
std::cout << "derived_" << b << std::endl;
}
};
int main() {
derived_class* p = new derived_class;
p->a = 1;
base_class* q = p;
q->member_function();
delete p;
}
->
や .
といった演算子を用いてメンバ関数を呼び出した場合、左オペランドのクラスの中から名前を探すということになっていて、動的な型についての言及はありません。
基底クラスのメンバが呼ばれるだけです。
デストラクタは外のメンバ関数と異なる
デストラクタはその本体を実行した後にデータメンバや基底の解体 (デストラクタの呼び出し) などを行います。 つまりプログラマが書いたデストラクタの中身が空っぽだったとしても暗黙にこれらの処理を含んでいるので適切に実行されないと破綻してしまいます。
デストラクタ以外では基底のメンバ関数が派生のデータメンバと作用することはないので不整合は生じないのですがデストラクタの場合は基底から呼び出した場合でも派生のデータメンバに作用する必要があるという点が異なります。
デストラクタが仮想関数になっていない場合はあくまで仕様上は未定義ではありますが普通の (デストラクタではなく仮想でもない) メンバ関数と同じようにデストラクタの挙動が実装されていたとしたら基底のデストラクタが呼び出されて (派生のデータメンバの解放なども担当する) 派生のデストラクタが呼び出されないという結果になるのは自然な動作です。
virtual
なら大丈夫
仮想関数をどうやって実現するかは言語仕様では規定していませんが、オブジェクトが隠れたデータメンバとして仮想関数テーブルへのポインタを保持するのが一般的なコンパイラでの実装です。 動的な型に対応した仮想関数テーブルのポインタをオブジェクトの生成時に格納することで基底のポインタを通じたメンバ関数呼び出しでもオブジェクト生成時の型に応じたメンバ関数を呼び出せます。
なぜか仮想関数テーブルの概略が Wikipedia に分かりやすく書いてあるので参考にするとよいと思います。