仮想デストラクタが無いと派生クラスのデストラクタが呼ばれない理由
基底クラスのポインタ経由で派生クラスのオブジェクトをdeleteしたとき、基底クラスのデストラクタが仮想(virtual)でないと、派生クラス側のデストラクタが呼ばれないバグです。
なぜエラーが出ないのか
出力: Base destroyed(Derived destroyedが呼ばれない)
(エラーなし)- C++は何も報告しません。コンパイルも実行も正常に完了するためです
出力: Base destroyed(Derived destroyedが呼ばれない)- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 #include <iostream> 2 class Base { 3 public: 4 ~Base() { std::cout << "Base destroyed" << std::endl; } 5 }; 6 class Derived : public Base { 7 public: 8 ~Derived() { std::cout << "Derived destroyed" << std::endl; } 9 }; 10 int main() { 11 Base* b = new Derived(); ^ 12 delete b; 13 return 0; 14 }
Baseのデストラクタがvirtualでないため、Base*経由でのdeleteはBaseのデストラクタしか呼びません。ポインタの型をDerived*にすれば正しい型のデストラクタから呼ばれます(本来の解決策はBaseのデストラクタをvirtualにすることです)。
直し方: Base* を Derived* にします。
パターン2
1 #include <iostream> 2 class Shape { 3 public: 4 ~Shape() { std::cout << "Shape destroyed" << std::endl; } 5 }; 6 class Circle : public Shape { 7 public: 8 ~Circle() { std::cout << "Circle destroyed" << std::endl; } 9 }; 10 int main() { 11 Shape* c = new Circle(); ^ 12 delete c; 13 return 0; 14 }
仮想関数として宣言されていないメンバ関数の呼び出しは、実行時の実際の型ではなく、ポインタの静的な型を基準に決まります。デストラクタも例外ではありません。
直し方: Shape* を Circle* にします。
パターン3
1 #include <iostream> 2 class Resource { 3 public: 4 ~Resource() { std::cout << "Resource destroyed" << std::endl; } 5 }; 6 class FileHandle : public Resource { 7 public: 8 ~FileHandle() { std::cout << "FileHandle destroyed" << std::endl; } 9 }; 10 int main() { 11 Resource* f = new FileHandle(); ^ 12 delete f; 13 return 0; 14 }
「deleteすればそのオブジェクトの型に応じたデストラクタが正しく呼ばれる」というのは誤解です。実際に呼ばれるファイルクローズ処理などが仮想デストラクタなしでは実行されず、リソースリークの原因になります。
直し方: Resource* を FileHandle* にします。
パターン4
1 #include <iostream> 2 class Device { 3 public: 4 ~Device() { std::cout << "Device destroyed" << std::endl; } 5 }; 6 class Printer : public Device { 7 public: 8 ~Printer() { std::cout << "Printer destroyed" << std::endl; } 9 }; 10 int main() { 11 Device* p = new Printer(); ^ 12 delete p; 13 return 0; 14 }
Deviceのデストラクタがvirtualでないため、Device*経由のdeleteはDeviceの分しか呼びません。
直し方: Device* を Printer* にします。
パターン5
1 #include <iostream> 2 class Account { 3 public: 4 ~Account() { std::cout << "Account destroyed" << std::endl; } 5 }; 6 class Savings : public Account { 7 public: 8 ~Savings() { std::cout << "Savings destroyed" << std::endl; } 9 }; 10 int main() { 11 Account* s = new Savings(); ^ 12 delete s; 13 return 0; 14 }
デストラクタの呼び出しも仮想関数と同じく、virtual宣言が無ければポインタの静的な型で決まります。
直し方: Account* を Savings* にします。
よくある誤解
「deleteすればそのオブジェクトの型に応じたデストラクタが正しく呼ばれる」というのは誤解です。仮想関数として宣言されていないメンバ関数の呼び出しは、実行時の実際の型ではなく、ポインタの静的な型(ここではBase*)を基準に決まります。デストラクタも例外ではありません。
実務での勘所
これはデストラクタに限った特別ルールではなく、C++の仮想関数全般に共通する仕組みの帰結です。virtualと宣言されていないメンバ関数の呼び出しは、コンパイル時にポインタの「静的な型」(ここではBase*)だけを見て、その型の実装に決め打ちで結びつけられます(静的束縛)。virtualと宣言された関数だけが、実行時にオブジェクトの実際の型を調べて正しい実装を探す(動的束縛、vtableという仕組みを使います)対象になります。デストラクタも1つの関数である以上、virtualを付けなければこの静的束縛の対象になってしまいます。