C++では基底クラスにvirtualデストラクタを書こう
(追記あり/再追記あり)
ブクマ経由で、C++で演算子オーバーロードしたときの演算子決定基準について調べたというのを見たのだけど、書いてあるサンプルコードが演算子オーバーロード以前にちょっとダメだった。
昔書いたテストコードと書いてあるので、今は分かってるのかもしれないけど、ある程度経験を積んだC++プログラマは絶対に(というのは言いすぎでした)virtualデストラクタのないクラスを継承しない(追記やTBやブコメの議論を参照のこと)ので、このサンプルコードを載せて違和感を感じない時点で、演算子オーバーロードをいじるよりもまずはEffective C++を読んだ方がよい。
何がダメか。以下のように、virtualデストラクタがないクラスを継承している。これはダメだ。例え基底クラスのデストラクタですべきことがないのだとしても、継承するつもりのあるクラスにはvirtualデストラクタを作らないといけない。 元記事の本題であるvirtualの挙動にも絡むのだけど、基底クラスにvirtualデストラクタがないと、deleteまたはスコープを外れて破壊されるときの型に応じてサブクラスのデストラクタが呼ばれたり呼ばれなかったりする*1。
何がダメか。以下のように、virtualデストラクタがないクラスを継承している。これはダメだ。例え基底クラスのデストラクタですべきことがないのだとしても、継承するつもりのあるクラスにはvirtualデストラクタを作らないといけない。 元記事の本題であるvirtualの挙動にも絡むのだけど、基底クラスにvirtualデストラクタがないと、deleteまたはスコープを外れて破壊されるときの型に応じてサブクラスのデストラクタが呼ばれたり呼ばれなかったりする*1。
#include<iostream> using std::cout; using std::endl; class BadBase { public : BadBase(){} //FIXME virtualデストラクタがないよ!! }; class BadSub : public BadBase{ public : BadSub(){ cout << "リソース確保しますた" << endl; } ~BadSub(){ //何かリソースを開放する cout << "リソース開放しますた" << endl; } }; int main(){ BadSub* sub = new BadSub(); //"リソース確保しますた" delete sub; //"リソース解放しますた" BadBase* base = new BadSub(); //"リソース確保しますた" delete base; //リソースが開放されない! return 0; }codepadで実行 これでは困る。では正しくはどうするか。 答え‥ 継承する可能性のあるクラスにはすべてvirtualデストラクタを作る
#include<iostream> using std::cout; using std::endl; class Base { public : Base(){} //例えすることがなくても、基底クラスにはvirtualデストラクタが必要 virtual ~Base(){} }; class Sub : public Base{ public : Sub(){ cout << "リソース確保しますた" << endl; } virtual ~Sub(){ //何かリソースを開放する cout << "リソース開放しますた" << endl; } }; int main(){ Sub* sub = new Sub(); //"リソース確保しますた" delete sub; //"リソース解放しますた" Base* base = new Sub(); //"リソース確保しますた" delete base; //"リソース解放しますた" return 0; }codepadで実行 C++では、virtualデストラクタのないクラスは継承するつもりのないクラス︵Javaでいうfinalクラス︶であると考えた方がよい。 逆に、C++でvirtualデストラクタがないクラスを継承しているコードを見たら︵というかvirtualデストラクタがないクラスを見たら︶そのコードは疑ってかかるべきである。 C++にはこのような落とし穴が山ほどある。Effective C++という書籍が、このような落とし穴を懇切丁寧に説明しているので、プログラミング言語C++が読みきれないと感じたとしても、C++プログラマはまずEffective C++を読むべきだ。<追記> コメントとかブコメとかTBで色々突っ込みをいただいたので反応。 ある程度経験を積んだC++プログラマは絶対にvirtualデストラクタのないクラスを継承しない? 継承前提でありかつ仮想デストラクタを書くべきでない反例のUncopyableクラスは端的で素晴らしいと思いました。 なんというか、僕は﹁実装継承(Private継承)はどちらかというと悪だ﹂みたいなイデオロギーを持っていた︵る︶ので、コピーコンストラクタや代入を禁止するのにPrivate継承を使うってのは考えたこともなかったです。確かにUncopyableポインタを介してオブジェクトを破壊することはなさそうなので素晴らしい例ですね。勉強になりました。 >ある程度経験を積んだC++プログラマは絶対にvirtualデストラクタのないクラスを継承しない そこまで言っちゃうとそれはそれでダウトw C++の根底の思想のひとつであるゼロオーバーヘッドの利益を享受する為に、直接 new/delete することはないクラスなら継承することが前提であってもデストラクタに virtual 指定はしませんよ。﹄( id:wraith13さん これはいい過ぎかなとも思ったのですが、そこまで細かい話を分かりやすく説明する自信がなかった&上記Uncopyableのような例を思いつかなかったのでこんな感じになりました。 僕は富豪的な環境でしか仕事してきてないので、vtblのメモリ格納や呼び出しオーバーヘッドが支配的になるような状況になるまでは、その辺は富豪的に行こうと思っていました。 結局C++を書かなくなるまでそういう状況に︵僕は︶ならなかったのでオーバーヘッドに関しては割と軽視していて、そこよりも多少のオーバーヘッドがあっても分かりやすくて自分の足を撃ち抜くことはないルールの方がいいかな、みたいに考えてました。 もちろんアーキテクチャ的に処理を分散するとか単純にスケールアップすることができる富豪的環境でしか通用しないルールなのでしょう。 C++はC++として使いましょう! とおりすがるさん C++ってあまりにも機能︵と落とし穴︶が多すぎるので、全機能を使い切ろうとはあまり思ってないのですよ。 vtblオーバーヘッドとかの話みたいに、それが必要になったときに落とし穴と一緒にちゃんと勉強すればいいかなと。 ﹃絶対に﹄は大嘘、これは落とし穴でもC++欠陥でもない。ストラウストラップがなにを考えてこういう仕様にしたのか知っとくべきだ。︵個人的にはvtblがないことを保障する修飾子か何かがあればよかったんだと思う︶ id:meg_nakagamiさんのブコメ 仰りたいことはid:wraith13さんと同じかな。僕の感想も同じ。あとvtblなしを保証する/指定する修飾子等があればよかったというのはすごい同意します。追記> ある程度の経験? std::unary_functionは知らなかったので勉強になります。C++をやってたころは関数型言語を全然知らなかったのでfunctionalヘッダに入ってるモノは全然チェックしてませんでした。 ただ、この例のstd::unary_functionとかstd::binary_functionとかは本当にvirtualデストラクタがない設計でいいの?という点はちょっと疑問でした。 例えばstd::unary_functionの派生クラスとして、﹁環境への参照を持つ関数(イメージとしてはクロージャ)﹂を作ってこいつはデストラクタで何かをしないとダメだとして、unary_functionオブジェクトとしてコンテナにつっこんで何かした後に、オブジェクトをunary_functionとして破壊すると不定な動作になるというのはちょっと不安に思います。 そういう使い方は想定外なんでしょうか。 絶対にというのはあからさまな言い過ぎにせよ、一般論として﹁継承するつもりのあるクラスでは基本的にvirtualデストラクタを用意せよ。そうしないのならば、自分が何をしようとしているか把握した上でせよ。﹂といったところならば妥当な落しどころといえそうですか?