FAQ形式でデストラクタにvirtualを付ける理由をまとめてみた

Q1: デストラクタにvirtualをつけろとよく言われるけど、なんで?

挙動が未定義のdeleteを呼び出す可能性があるから

Q2: 挙動が未定義だと駄目なの?

超駄目。何が起きても文句は言えない

Q3: どんな時に挙動が未定義のdeleteが呼び出されるの?

こんなとき

// NoVirtualBaseClass : デストラクタにvirtualを持たない
// NoVirtualSuperClass : NoVirtualBaseClassを継承
NoVirtualBaseClass * ptr = new NoVirtualSuperClass();

 // 挙動が未定義
delete ptr; 

Q4: 何でそうなるの?

delete時点で、ptrは自分がNoVirtualBaseClassのインスタンスだと思ってるから

Q5: 何で?NoVirtualSuperClassでnewしたのに

C++インスタンスは、標準では、自分の動的な型情報を持ってない
分かるのは、宣言時の型情報だけ
だから、deleteの時に本当はNoVirtualSuperClassのインスタンスなのに、NoVirtualBaseClassのデストラクタしか呼ばれない
この時の挙動が未定義
大抵は、NoVirtualSuperClassの差分だけメモリリークだろうけど、他のインスタンスがぶっ壊れたり、何が起きても文句は言えない

Q6: そうしない為にデストラクタにvirtualが必要なのね

そういうこと

Q7: でも何でデストラクタにvirtualをつけると大丈夫なの?

virtual付きのメソッドを持つと、コンパイラからこのクラスは継承されると認識されて、実行時型情報を持つようになる
実行時型情報があると、deleteの時に自分が何でnewされたか参照できるので、正しいデストラクタが呼ばれるようになる

Q8: だったら最初から全部のクラスに実行時型情報がついてりゃいいのに

2つの理由から無理

後方互換性確保のため


例えば、インスタンスのサイズを意識した実装をしている場合

Test * test = new Test(); // サイズが64bit 

// 指定のサイズのデータを送信
// 受信も先でまたキャストしてtestに戻す
sendData( ( * void ) test, 64 );


こんなのがあると、もしC++の仕様が変わって全部のクラスに実行時型情報がついちゃうと、挙動がおかしくなるから

C++の設計思想のため

使わない機能はコストを発生させない(ゼロオーバーヘッドルール)

C++の設計と進化
4.5 低レベルプログラミングをサポートするためのルール(P.151)


継承しないのに実行時型情報を持ってるのは、サイズや速度の面で無駄なので、この思想に反してる

Q9: 低レベルプログラミングや、後方互換性の話だから、普通のPCだけで使う分には全部にvirtual付けても、問題ない?

わりとそう
組み込みみたいな環境でなければ、速度やサイズに有意な差は出ないみたい
RTTI のコストを理解する
なので、全てのデストラクタに無条件にvirtualをつけると言うのも一つの答えだと思う

Q10: なんか物が挟まった言い方だね

Effective C++だと全部にvirtualを付けるのは推奨してないしね…
理由は、サイズと処理の増加を嫌って

Q11: でも、全部にvirtual無いと、間違いで非仮装クラスを継承されたとき危なくない?

コンパイル時にエラーを出す方法があります
コンストラクタをprivateにしちゃえば、継承しても、コンパイル時に以下のエラーが出ます

error C2248: privateメンバにアクセスできません。


これは、継承したクラスのコンストラクタでは、暗黙で最初に基底クラスのコンストラクタを呼ばなきゃいけないけど、コンストラクタがprivateになると、それが出来なくなるから

Q12: これやると普通のnewもできなくね?

singleton、factoryパターンとかでポインタを渡す様にすればOK
それぞれ概要を書くけど、詳しくは解説書を参考に

singletonパターン

あるクラスのインスタンスは1つしか作らせないパターン

class Single
{
private : 
  static Single * instance = 0x00;
  Single( void ){} // コンストラクタはprivate

public :
  // ポインタを取得
  static Single * getInstance( void ){
    if( instance == 0x00 ){
      instance = new Single();
    }
    return instance;
  }
};
factoryパターン

要求されると、インスタンスを生成するパターン

class SimpleFactory
{
private : 
  SimpleFactory( void ){} // コンストラクタはprivate

public :
  // ポインタを生成
  static SimpleFactory * makeInstance( void ){
    return new SimpleFactory();
  }
};

Q13: ポインタを渡すと、deleteが呼び出した人の責任になるから怖い

proxyパターンでポインタを実体に隠蔽して渡せばOK

// newとdeleteを隠蔽する事で、ポインタの存在を意識させない
class ProxySimpleFactory
{
private : 
  SimpleFactory * ptr;

public :
  ProxySimpleFactory( void ){} 

  ~ProxySimpleFactory( void ){
    if( instance != 0x00 ){
      delete instance;
    }    
  } 

  // 本当はインターフェースを作って
  // SimpleFactoryの持つメソッドを持って
  // 使う人からは、ポインタを隠蔽した方がベター
  SimpleFactory * getInstance( void ){
    if( instance == 0x00 ){
      instance = SimpleFactory::makeInstance();
    }
    return ptr;
  }
};

Q14: そこまでして継承を禁止するのは逆にオーバーヘッドでない?

俺もそう思う。
実際の所は非virtualにしておいて、規約で回避するのが一番だと思います。
二番手は、全部virtualにしちゃう。
実装時のメンバーのレベルで決めればよいと思う。

経緯

一ヶ月前からQ9以降がわかんなくて、こないだやっと分かったのでまとめました。


そんなかんじです。