ゆとりーなの日記

日記的な事を書いて行くと思はれる

仮想デストラクタ、御呼びじゃない?

少し前にクラスのデストラクタにvirtualを付けるかどうかが話題になっていました。継承され得るクラスの作法の話など。 - 偏見プログラマの語り!
仮想デストラクタはコストがかかりますから、virtual付けなくて済むなら付けたくないのがCプラーの人情というものでしょう。
ここではデストラクタにいつvirtualを付けるべきかに付いて少し考えてみます。
まずはデストラクタにvirtualを付けないとどういう問題が起きるかをおさらいしておきます。

class base {
 public:
  ~base {}
};

class derived : public base {
 public:
  derived() : p_(new int[100]) {}
  ~derived() {delete []p_;}
 private:
  int *p_;
};

int main() {
  base *p = new derived();
  delete p; // derived::~derivedが呼ばれないかもしれない→豊かなメモリーリーク
  return 0;
}

こんな感じに、基底クラスのポインタ経由でdeleteした時に派生クラスのデストラクタが呼ばれない可能性があります。~derivedにvirtualを付けても駄目です。この場合はderived*経由で派生クラスのポインタをdeleteした時に派生クラスのデストラクタが呼ばれることが保障されるだけですので何の解決にもなっていません。
さて、現行C++ではどんなクラスも継承できるのでvirtualが付いてないクラスを継承した場合はこの手の問題を引き起こす可能性を秘めていることになります。しかし、だからといってデストラクタ全部にvirtualを付けて回るのも甘えでしょう。色々な事例からどのような場合にvirtualが付いていて、どのような場合に付いていないかを見てみるとvirtualを付けるヒントになると思います。この際は標準ライブラリや準標準ライブラリであるBoostを参考にするといいと思います。

virtualをつけるべき例「仮想関数があるクラス」

これは当たり前です。仮想関数があるということは当然多態を意識したクラスであるわけで、基底クラスのポインタ経由で派生クラスをdeleteすることも当然行われるわけです。これでデストラクタがvirtualでなかったらキレていいレベルです。

virtualをつけた方がいい例「仮想関数がないけど継承してもいいよなクラス」

この場合はそのクラスのポインタ経由では多態も出来ませんし、正直派生クラスのポインタをぶち込んで操作するメリットは殆どないのですが、一応そういう操作がされないとは言い切れないので念のためvirtualをつけておいた方がいいと思います。

virtualを敢えて付けない例「仮想関数がないんだから継承するななクラス」

標準コンテナなんかがこれに当たるのかなと思います。まぁ仮想デストラクタはコストがかかりますから、仮想関数がないクラスは俺は俺で完結してるから継承なんかする必要ないでしょと主張してもいい気はします。C++0xでは継承禁止のキーワードが入りますから、この手のタイプのクラスにはこれを付けて置くのがいいのかもしれません。まぁもちろん基底クラスのポインタにアップキャストできなければいいという考え方も出来るので、private継承なんかはアリだと思うのであればこの辺は好みの問題になるんですかね。

virtual不要な例「継承されることが前提だけど、俺typedef位しか持ってねーよなクラス」

所謂std::unary_functionとかがこれですね。これらは継承することで継承したファンクタの引数や戻り値の型情報を管理してくれます。まぁ0x環境だと殆どいらない子になりそうな感はありますがその辺は触れないでおきましょう。ファンクタはコピー前提なので出来る限り小さい必要があるのでやっぱり仮想デストラクタはお邪魔になってしまうんですね。そしてこれに関しては基底クラスのポインタを使っても殆ど何にも出来ません。流石にこれを使って派生クラスのポインタをどうにかしようと思う人はスルーしてもいいでしょう。逆にもしこれを使ってなにかしようと思う人がいたら(ひょっとするとファンクタの型情報を消すTypeErasureとかに使えるかもしれない?)多分その人は相当分かっているので仮想デストラクタ絡みのヘマはやらかさないでしょう。

virtual不要な例「継承されることが前提だけど、俺からっぽだよなクラス」

タグ構造体とかですね。tagを引数に使って、tagの型で呼び出す関数を分岐する時なんかに使ってたりします。なんとか_iterator_tagとかはほんとに空っぽです。この子達も継承が前提で使われますが、やっぱり空っぽのクラスのポインタでは何もできないので仮想デストラクタは不要でしょう。

virtual不要な例「継承されることが前提だけど、メンバは全部privateだよなクラス」

Boostのiterator_adaptorsとかがこれみたいです(これについてはちょっと嘘入ってました。更に追記参照。)。イテレータもコピー前提なのでやっぱり小さいほうがいいというのが理由かと思います。これらは見た感じメンバが全部privateで専用のアクセスクラス経由でメンバを扱うみたいです。というわけで基底クラスのポインタ単体では殆ど何も出来ず、無理に何かをしようと思っても、物凄く手間がかかるので普通はこれで何かしようとは思わないってことになるのだと思います。

なんかvirtualが付かない例「CRTPなクラス」

静的多態とかの一手段に使われることがあるCRTPなクラスでデストラクタにvirtualが付いている例ってのは見たことがないです。まぁこれ、一応基底クラスポインタで色々できてしまいますが、使い方が明示されているし、且つその使い道が静的多態なのに態々動的多態に持ち込む人はいないだろうってことなんですかね。

何故かvirtualが付かない例「一部のCOMインターフェイスクラス」

これ、メソッドが全部純粋仮想関数で、派生クラスで全メソッドをオーバーライドする所謂インターフェイスクラスなんですが、仮想デストラクタが何故かないです。インターフェイスだからprivate継承してねってことなんですかね。

総括

大分個人の主観とかが入っているので突っ込み等あればコメントなりtwitterなりでどんどん突っ込んでください。
追記:
Boostのnoncopyableみたいなクラスも仮想デストラクタは要らないよねって辺りの話を忘れてました。他にも漏れがありそうな・・・。
更に追記:
Boostのiterator_adaptors周りのところ、よく見たらprivate以外のメソッドもありました。なんでちょっとこの例からは外れてしまうっぽいです。