仮想デストラクタ|virtualデストラクタの目的や問題、回避方法

仮想デストラクタとは

仮想デストラクタ(virtualデストラクタ, virtual destructor)は仮想関数として宣言されたデストラクタのことです。仮想デストラクタはvirtual指定子を用いて宣言・定義されます。

struct Base {
   // 仮想デストラクタ
   virtual ~Base() { puts("~Base"); }
};

目次

仮想デストラクタの目的

仮想デストラクタはクラスのポリモーフィックな挙動を実現する目的で利用されます。仮想デストラクタを基底クラスで適切に宣言すれば、派生クラスのインスタンスを基底クラスのポインタとしてdeleteした場合でも、派生クラス側のデストラクタが正しく呼び出されるようになります。

// 基底クラス //
struct Base {
   virtual ~Base() { puts("~Base"); }
};

// 派生クラス //
struct Derived : Base {
   ~Derived() { puts("~Derived"); }
};

// 派生クラス → 基底クラスへのアップキャスト
Base *a = new Derived;
// 解放
delete a; // 出力: "~Derived\n~Base\n"

このように、派生クラスから基底クラスへのアップキャスト後のオブジェクトに対する解放処理を行った場合でも、派生クラス側のデストラクタがきちんと呼び出されるようになります。

これはまさに、virtualによって宣言された仮想デストラクタの効果であり、一般的な宣言方法ではこのような挙動はとられません。

仮想デストラクタ未使用時の問題

基底クラスのデストラクタが仮想関数としてではなく、一般的なデストラクタとして宣言された場合、ポリモーフィズムが実現されず、派生クラス側のデストラクタが呼び出されなくなります。

struct Base {
   /*virtual*/ ~Base() { puts("~Base"); }
};

struct Derived : Base {
   ~Derived() { puts("~Derived"); }
};

Base *a = new Derived;
delete a; // 出力: "~Base\n"

派生クラス側のデストラクタが呼び出されなくなるということは、つまり派生クラス側のリソースの解放処理が働かなくなるということです。動的に確保したメモリーの解放処理が行われなくなったり、ファイルポインタの閉じ忘れなどが起こる原因となります。

struct Derived : Base {
   int *ptr;
   Derived() : ptr(new int(9)) {} // メモリの動的確保
   ~Derived() { delete ptr; }     // メモリの解放
};

Base *a = new Derived;
delete a; // 派生クラス側のデストラクタは呼ばれないため、
          // `new int(9)`の実体は解放されないままとなる

これは上記サンプルのような動的確保されたオブジェクトとそのポインタ変数だけの問題ではなく、一般的なメンバ変数についても大きな問題となります。メンバ変数側のデストラクタが呼ばれなくなるためです。

struct Derived : Base {
   std::string str = "string";
};

delete static_cast<Base*>(new Derived);
// デフォルトのデストラクタも呼ばれなくなるため、
// メンバ変数`std::string str`のデストラクタも呼ばれなくなる

メンバ変数のデストラクタ呼び出しは、メンバ変数を保有するクラス側のデストラクタ内で暗黙的に行われます。そのため、基底クラス側でvirtualデストラクタを宣言しないと、派生クラス側のメンバ変数に対する暗黙的なコンストラクタ呼び出しも行われなくなってしまいます。

特にC++の文字列型(std::string)は内部で動的なメモリ確保を行っているため、std::string型メンバ変数のデストラクタが呼ばれなくなると、メモリの解放が行われず、メモリーリークが発生することに繋がります。

このように派生クラス側のデストラクタが呼び出されなくなると、重大な問題を引き起こす原因ともなります。そのため、ポリモーフィズムを前提としたクラスの活用を行う際には、基底クラスに対する仮想デストラクタの宣言が必須となります。

仮想デストラクタの問題点

仮想デストラクタは仮想関数テープル(vtable)という仕組みを用いて実現されています。vtableはオブジェクトサイズの肥大化を招いたり、オブジェクトの初期化時や解放時、メンバ関数呼出し時のオーバヘッドを生み出したり、コンパイラ側の最適化(関数呼び出しのインライン化等)を阻害する原因となります。高いパフォーマンスが要求される場面では、これらの特性が問題となる場合があります。

そのため、無闇な仮想デストラクタ宣言は、あまり好ましいものとは言えません。よって、仮想デストラクタを使うべき場面と、使うべきではない場面を的確に判断し、仮想デストラクタを明確に使い分ける必要があります。両者の判断基準については次節以降を参考にして下さい。

仮想デストラクタの回避方法

final指定子による派生の制限

継承を行わないクラスに関しては、仮想デストラクタの宣言は不要です。そのようなクラスについては、finalキーワードでクラスの派生を制限することが有効です。

struct Base final {};
struct Derived : Base {}; // エラー: Base 'Base' is marked 'final'
                          // 継承できなくなる

protectedによる解放処理の制限

基底クラスに対する解放処理を行わない場合も仮想デストラクタの宣言は不要です。そのようなケースでは、基底クラスのデストラクタに対するアクセス制御をprotected指定で行うことが有効です。

struct Base {
protected:
   ~Base() { puts("~Base"); }
};

struct Derived : Base {
   ~Derived() { puts("~Derived"); }
};

Base *a = new Derived;
delete a; // エラー: Calling a protected destructor of class 'Base'
Base b; // エラー: Variable of type 'Base' has protected destructor

delete new Derived; // 出力: "~Derived\n~Base\n"

これで、基底クラス側に対する明示的な解放処理を制限することができます。ただし、基底クラス自身のインスタンス化が行えなくなる点には注意が必要です(Base b;)。

仮想デストラクタが必要な場面

ポリモーフィズム(多態性)の実現を前提とするクラス設計では、大抵は仮想デストラクタの宣言が必要になります。仮想関数や純粋仮想関数が宣言されたクラスがその良い例です。また# 仮想デストラクタの目的の例のように、派生クラスを基底クラスへとアップキャストした状態で、基底クラスに対する解放処理を行うような場面では、仮想デストラクタの宣言は必須となります。

また、派生クラス側での意図しないデストラクタ宣言に対処するために、基底クラス側で予め仮想デストラクタを宣言しておく場合もあります。これは、仮想関数の利用によるオーバヘッドの増加よりも、デストラクタが呼ばれなくなることによるリスクを重要視する際に有効な手段となります。

なおC++では、デストラクタに対するfinal指定子の利用が的確に行えないため、派生クラス側のデストラクタ宣言を制限することができません。
広告