*「ふっかつのじゅもんがちがいます。」withぬこ このページをアンテナに追加 RSSフィード

はてなRSSで購読 Bloglinesで購読 Google Readerで購読

2008-09-25

C++では基底クラスにvirtualデストラクタを書こう

(追記あり/再追記あり)

ブクマ経由で、C++で演算子オーバーロードしたときの演算子決定基準について調べたというのを見たのだけど、書いてあるサンプルコードが演算子オーバーロード以前にちょっとダメだった。

昔書いたテストコードと書いてあるので、今は分かってるのかもしれないけど、ある程度経験を積んだC++プログラマ絶対に(というのは言いすぎでした)virtualデストラクタのないクラスを継承しない(追記やTBやブコメの議論を参照のこと)ので、このサンプルコードを載せて違和感を感じない時点で、演算子オーバーロードをいじるよりもまずはEffective C++を読んだ方がよい。


何がダメか。以下のように、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デストラクタを用意せよ。そうしないのならば、自分が何をしようとしているか把握した上でせよ。」といったところならば妥当な落しどころといえそうですか?

</再追記>

*1:現実はもっと非情であり、この場合の動作は不定である。Effective C++第二版の表現を借りれば「C++標準規格は、この点について極めて明確に述べている。基底クラスのポインタを介して派生クラスのオブジェクトを削除しようとするとき、その基底クラスに仮想でないデストラクタがあると、その結果は不定となる。不定とはすなわち、コンパイラはどんなコードでも好き勝手に生成していい、という意味である。」鼻から悪魔が飛び出ます。

wraith13wraith13 2008/09/25 06:43 >ある程度経験を積んだC++プログラマは絶対にvirtualデストラクタのないクラスを継承しない

そこまで言っちゃうとそれはそれでダウトw
C++の根底の思想のひとつであるゼロオーバーヘッドの利益を享受する為に、直接 new/delete することはないクラスなら継承することが前提であってもデストラクタに virtual 指定はしませんよ。

とおりすがるとおりすがる 2008/09/25 09:52 >ある程度経験を積んだC++プログラマは絶対にvirtualデストラクタのないクラスを継承しない
C++を不便なJavaとして使っちゃってる人ならこれは真かも。
C++はC++として使いましょう!

とおりすがるとおりすがる 2008/09/25 16:12 >全機能を使い切ろうとはあまり思ってない
あー、私が問題としたのは、機能じゃなくてパラダイムです。
機能とパラダイムの間には少なからず関係があることは事実ですが。

仮想デストラクタがないクラスを継承するな、というのは継承をまさにJava(や他の多くのオブジェクト指向言語)の考え方をそのままC++に持って行ったために起こる問題(問題?)ですよね。
でも、C++にはそういう継承の使用方法だけでなく、多態を前提としていない継承も存在します。
Isoparametricさんやmeg_nakagamiさんが示してくれた例がそれにあたりますよね。

これを機能と呼ぶのは間違いとは言わないでしょうけど、やっぱり*パラダイム*なんですよね。
別にオブジェクト指向言語としてのみC++を使うこともできるのですが、C++はマルチパラダイム言語ですから、やっぱりいろいろなパラダイムを意識して使ったほうがよりC++らしいプログラムとなるはずです。
結果できあがったプログラムがキメラのように見えたとしても、そこはC++(しーたす)への愛でカバーです。

ajiyoshiajiyoshi 2008/09/25 16:42 とおりすがるさん

パラダイムという点でいうと、オブジェクト指向パラダイムも関数型プログラムパラダイムもテンプレートによる総称型プログラムパラダイムも、どれも僕は受け入れる用意があります。

しかし、受け入れられないのは「C++プログラマはC++の仕様に関して全知全能であるはずであり、常にプログラム全体の中で自分が何をしようとしているのか把握しているものである」とでもいうべきパラダイムです。

僕がstd::unary_functionの存在を知らずに、同じようなmy::unary_functionみたいなクラスを作ってしまったならば、おそらくvirtualデストラクタを用意します。理由は再追記に書いたように、「もしかしたら誰かが派生オブジェクトをmy::unary_functionとして破壊して自分の足を撃ちぬくかもしれないから」です。オーバーヘッドはあるけれど、フールプルーフであり安全サイドに倒すパラダイムです。そういう意味で元記事の http://local.joelonsoftware.com/mediawiki/index.php/%E9%96%93%E9%81%95%E3%81%A3%E3%81%9F%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AF%E9%96%93%E9%81%95%E3%81%A3%E3%81%A6%E8%A6%8B%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B これに近いイデオロギーだと思います。C++はこういうパラダイムで作られていないのは知っているので、運用で対応しているのです。

wraith13wraith13 2008/09/27 17:51 >「継承するつもりのあるクラスでは基本的にvirtualデストラクタを用意せよ。そうしないのならば、自分が何をしようとしているか把握した上でせよ。」

あえてこの表現使うけど、それは「C++を不便なJavaとして使っちゃってる人なら」それで正しいかもしれません。
C++を扱った話題をしてるのに視点がJava脳のまんまなのですよ、それでは。
C++脳的な視点に立てば「派生クラスをそのクラスのポインタを介して解放(デストラクト)する予定の時はデストラクタを virtual にする。」で、それ以上でもそれ以下もないわけです。

ajiyoshiajiyoshi 2008/09/28 16:10 Java脳といえばJava脳かもしれません。でも日常的にJava書いてるわけでもないので、どちらかというとLL脳かな。LLでも実現可能な仕事ばかりしているので、書くのは主にLLですし。
「多少遅くても書きやすく読みやすいコードを書け。最適化はプロファイルをしてからでも遅くはない。どうしても速度が必要ならC/C++で書け」みたいなことを思っているので、確かにJava脳/LL脳と言われてもおかしくないですね。

今現時点で、C++が使われている状況というのは、「多少危険だろうが必要のないオーバーヘッド(例えば仮想でなくてもよい場合の仮想関数経由の関数呼び出し)を排除せよ。とりわけタイトなループの途中で20ナノセカンド節約できる場合には」ってな感じなのですかね?

meg_nakagamimeg_nakagami 2008/09/28 19:08 まったくちがう。節約の問題ではない。
テンプレートメタプログラムの視点からはvtblは『邪魔』なことがある。
実行時のスピードのパフォーマンスの話ではなくて、存在そのものが邪魔になる。
vtblがない方がいいのではなくて『存在してはならない』パターンもある。

Java的考えでは実行時のポリモーフィズムで解決するもののうちのある種のものを
C++ではコンパイル時にテンプレートで解決させる(という考え方がある)。
この辺は(あえて悪い言葉を使うと)Java脳のひとには理解できない。

ajiyoshiajiyoshi 2008/09/29 00:01 なるほど。テンプレートメタプログラミングですか。

テンプレートメタプログラミング的な所は全然たしなんでいなくて、アッカーマン関数をテンプレートメタプログラミング的に計算させてしてコンパイル終わらないね、みたいなのを試したくらいです。実際のところ仮想関数による実行時の多態とテンプレートメタプログラミングは相容れないですね(多分。生半可な知識で言ってます)。

virtualがデフォルトではないC++ではこういうこともできるというのは美点であると思います。でもこの辺の世界は
「継承するつもりのあるクラスでは基本的にvirtualデストラクタを用意せよ。そうしないのならば、自分が何をしようとしているか把握した上でせよ。」
>自分が何をしようとしているのか把握した上
な世界だと、私は思いますけどね。

milkmilk 2009/09/19 17:51 とても勉強になりました。ありがとうございます。

風 2010/06/28 11:04 その前にBadBaseとBadSubのクラス名称が見た目似ていてしばらく勘違いしました。
読み手の気持ちになって読みやすいコードを書くのが一番だと思います。
C++というのどういう考え方でも書ける柔軟さをもった言語なのでvirtualデストラクタが必須なら
それでよいしそうでなければそれでよいし書く人にまかされていると思います。
問題は読みやすいコードを書くこと。それに尽きると思います。たとえクラス名一つでも。

リンク元
Connection: close