C++で仮想デストラクタを使うべき場面とは?ポリモーフィックな基底クラスの注意点
『Effective C++』におけるスコット・マイヤーズの助言
スコット・マイヤーズは名著『Effective C++』の中で、仮想デストラクタについて次のように述べています。
「クラスが仮想関数をひとつでも持つなら、そのクラスは仮想デストラクタを持つべきである。一方、基底クラスとして設計されていないクラスや、ポリモーフィックに扱われることを意図していないクラスは、仮想デストラクタを宣言すべきではない」
つまり、ポリモーフィックな基底クラスでは、デストラクタを必ず仮想関数として宣言するのが原則です。
仮想デストラクタがないと何が起きるのか
問題が発生するのは、派生クラスのコンストラクタでオブジェクトを生成し、それを基底クラスのポインタ経由で管理するようなケースです。以下のコードを見てみましょう。
Base* b = new Derived(); // b を使用する処理 delete b;
ここで Base のデストラクタが仮想関数でない場合、delete b の動作は未定義(undefined behavior)になります。これは、デストラクタの呼び出しが他の非仮想関数と同じように静的に解決されるためです。
その結果、呼び出されるのは Base のデストラクタだけであり、Derived のデストラクタは実行されません。派生クラス側で確保したメモリやファイルハンドルなどのリソースが解放されず、リソースリークが発生してしまいます。
仮想デストラクタを正しく使った例
逆に、基底クラスのデストラクタを virtual にしておけば、delete b の時点で実際のオブジェクト型(Derived)のデストラクタが動的に解決され、まず派生クラス、続いて基底クラスのデストラクタが正しい順序で呼び出されます。
class Base {
public:
virtual ~Base() = default; // 仮想デストラクタ
};
class Derived : public Base {
public:
~Derived() override {
// リソースの解放などの後始末
}
};
仮想デストラクタを付けるべきでないケース
一方で、継承やポリモーフィズムを前提としないクラスにまで安易に仮想デストラクタを付けるのは避けるべきです。仮想関数を持つクラスには通常 vtable(仮想関数テーブル)へのポインタが追加され、オブジェクトサイズの増加やわずかなパフォーマンス低下を招くためです。基底クラスとして使う予定のないクラスは、余計な仮想デストラクタを宣言せず、シンプルな設計のままにしておきましょう。
まとめ
- 仮想関数を1つでも持つクラス(=ポリモーフィックな基底クラス)には、必ず仮想デストラクタを宣言する。
- 仮想デストラクタがないと、基底クラスのポインタ経由での
deleteが未定義動作となり、リソースリークの原因になる。 - 基底クラスとして設計されていないクラスには、vtable のオーバーヘッドを避けるため仮想デストラクタを付けない。
-
C/C++の前方宣言(フォワード宣言)とは?使える場面と書き方を解説
C/C++における前方宣言(フォワード宣言)とは、クラスや関数の完全な定義よりも前に、その存在だけをコンパイラへ伝える仕組みです。前方宣言を行うことで、それ以降のコードは「Personという名前のクラスが存在する」ということを認識できるようになります。コンパイラはその名前を見つけてもエラーを出さずに処理を進められ、実際のクラス定義は後の段階でリンカによって解決されます。前方宣言の基本的な例class Person; void myFunc(Person p1) { // ... } class Person { // ここにクラス定義を記述 };このコードがどう動くのかこの
-
C++のfriend(フレンド)関数とは?必要になる場面と使い方を解説
C++におけるfriend(フレンド)関数は、クラスのスコープ外で定義される関数でありながら、そのクラスのすべてのprivateメンバーおよびprotectedメンバーにアクセスする権限を持つ特別な関数です。フレンド関数のプロトタイプ宣言はクラス定義内に記述しますが、あくまで「宣言」だけであり、フレンド関数自体はそのクラスのメンバ関数にはなりません。 フレンドとして指定できるのは、通常の関数だけでなく、関数テンプレート、メンバ関数、さらにはクラスやクラステンプレートも可能です。クラスをフレンドとして指定した場合、そのクラス全体とそのすべてのメンバがフレンドとなります。 friend関数の宣言方