C++ でデストラクタを virtual にしなくてはならない条件と理由
Google C++ Style Guideにも書かれているように、クラスに仮想メンバー関数が存在する場合、そのクラスのデストラクタは virtual でなくてはなりません (If your class has virtual methods, its destructor should be virtual.)。 ただその理由は若干複雑です。理由が説明できないとコードレビューで問題を指摘する際に困りますし、逆に必要ないのに 「デストラクタには常に virtual をつけろ」と言われた場合に反論できなくて困ることになります。
ルールの前提
virtual なメソッドがあるなら、子クラスのポインタは必ず親クラスのポインタとして使用される
そもそも virtual なメソッド (仮想メンバー関数)がクラスに定義されているということは、そのクラスは継承した子クラスを作成し、その子クラスのインスタンスは親クラスのポインタに格納して利用するはずです(ポリモーフィズム) 。 そうでないならば、メソッドを virtual にする意義がそもそもありません。 virtual なメソッドがクラス定義されていると、vtable へのポインタの分だけインスタンスのサイズが増えますし virtual なメソッドの呼び出しは vtable を利用する分遅くなります。 そのためプリモーフィズムを利用しないクラスであれば、不必要に virtual をつけるべきではないでしょう。
子クラスのポインタを親クラスのポインタにキャストして使用するならデストラクタが virtual でなくてはならない
言い換えると、デストラクタが virtual
でない場合、子クラスのポインタを親クラスのポインタにキャストして使用してはいけません。
なぜならデストラクタが virtual でない場合、親クラスの型のポインタを
delete
した際には親クラスのデストラクタしか呼ばれないからです。
たとえ親クラスの型のポインタが指している実体が子クラスだったとしても子クラスのデストラクタ
(これは暗黙的に親クラスのデストラクタを呼ぶ) は呼び出されません。
// ~Parent is not virtual.
Parent* parent = new Child();
...
delete parent; // this always calls ~Parent(); ~Child() is never called.
このコードでは Child
のデストラクタが呼び出されないので Child
のリソースの解放が行われません。 もちろん delete parent;
を呼ばなければ問題は起こらないので、 delete parent;
しないように気をつけていれば Child のポインタを Parent*
に代入すること自体は問題ないが、
間違えやすい上に間違えた場合にはメモリーリーク系の厄介なバグの原因になるので
親クラスのポインタにキャストすること自体を避けるべきです。
なお、暗黙的に作られるデストラクタは virtual ではないのでデストラクタが空の場合でも virtual なデストラクタを明示的に定義しなくてならない点に気をつけて下さい:
class Parent {
...
// 例えデストラクタが空でも
// virtual なデストラクタは明示的に定義する
virtual ~Parent() {}
}
結論
上で述べた 2 つの前提
- virtual なメソッドを持つクラスは、その子クラスのポインタを親クラスのポインタとして必ず利用するが、
- 子クラスを親クラスのポインタとして利用する場合、デストラクタは virtual でなくてはならない
から 「virtual なメソッドを持つクラスのデストラクタは virtual でなくてはならない」 というルールが必要であることが分かります。
おまけ: 派生系のルール
「C++では常にデストラクタは virtual にすべき」というルールでないのは virtual 関数は vtable を使用するため、 virtual のデストラクタは呼び出しが若干遅くなるのと、 virtual があることでインスタンスのサイズが vtable へのポインタ分増えてしまい無駄だからです。そもそも デストラクタがデフォルトで virtual でないのは意図的なものです。 しかし多くのプログラムではこの 2 つによるパフォーマンスの劣化はそれほど深刻にはならないと思いますし、 その点を理解した上で「常にデストラクタを virtual にする」というシンプルで覚えやすいルールで運用するのはありえなくはないかと思います。 でもそういうプログラムでは C++をそもそも使わないかもしれません。
また「継承される可能性のあるクラスのデストラクタは virtual にすべき」というルールが書かれていることもありますが、 このルールだと継承はするけれど、親クラスのポインタへのキャストをしない場合には virtual をつけることで無駄なコストが発生してしまいます。 一方で、このルールのほうが virtual なメソッドは無い (ポリモフィズムは使わない) が子クラスのポインタを親クラスのポインタとして利用することはある場合 (あまりないと思うが) にも対応できるので 優れているとも言える。 いずれにせよ現実には、継承は多くの場合ポリモフィズムとセットで利用されるので、 「継承される可能性のあるクラス」と「virtual なメソッドを持つクラス」は同義である場合が多いかと思いますし、 個人的には 「継承される可能性のあるクラスのデストラクタは virtual でなくてはならない」と 「virtual なメソッドを持つクラスのデストラクタは virtual でなくてはならない」は 実用上は差はないかと思います。