Win2000 VC++6.0 SP5 MFC使用です。
「これができなくてコマってます。どーしたらよいか教えて下さい」
といった類の質問ではないので、雑談掲示板向けですが、技術情報
なので、一応こっち。
try catch の 使用法なのですが、皆様なにかルールのようなものを
設けてらっしゃいますでしょうか?
小生モノグサ故、一番外側の関数(イベントハンドラとか、人様に
提供して直接 call してもらう関数)には、発生した例外を全て
catch するため、catch (...) ブロック入れているくらいなもので
特にポリシーとか、教条とかは持ってません。
「例外処理をどのように書いたらよいか」ということを考えたとき、
解らない(というか迷う)のは以下の通り。
1. 全ての関数に try, catch を入れるべきか?
2. catch したとき、通常のルートに戻すべきか? それとも上位関数に
さらに例外を投げるべきか?
3. try, catch をネストさせるべきときはどのようなときか?
4. 例外クラスの定義はどのようにやるべきか?(一説にはエラー1つに
対し、例外クラスを1つ定義するべきだ、って聞いたこたります)
余談ですが、try, catch の作法について書いてある書籍って少ない
ような?(文法説明して終わり、というケースが殆ど)
これを読んだ方のうち、「私はこうゆうふうにしてる」ってポリシーを
お持ちの方、コメントいただけるありがたいです。
> 1. 全ての関数に try, catch を入れるべきか?
catchして為すべき事がある場合だけcatchします。
例外機構というのは、エラー処理を一ヵ所にまとめるための
仕組みだと思います。
> 2. catch したとき、通常のルートに戻すべきか? それとも上位関数に
> さらに例外を投げるべきか?
これは関数の仕様次第ですね。
> 3. try, catch をネストさせるべきときはどのようなときか?
ネストするかしないかを悩むような場面はちょっと思いつきませんが…。
> 4. 例外クラスの定義はどのようにやるべきか?(一説にはエラー1つに
> 対し、例外クラスを1つ定義するべきだ、って聞いたこたります)
エラーごとにクラスが分けてある(しかも、ちゃんと階層化されている)方が
try/catch構文を使ってエラー処理の仕分けができるので便利だと思います。
ですので、広く一般に使われるライブラリ等では
ちゃんとエラーごとに分けておいた方が良いでしょうね。
特定のアプリケーションで使われるサブシステムならば、
処理方法が異なるエラーごとに分かれていれば充分です。
こんばんは! とま吉です。
興味深いテーマなので、横から失礼いたします。(^^;
実は以前、他の掲示板でも似たような質問をさせていただいたことがあるのですが、
その後、いまいちしっくり来ていないところも出てきました。
try ~ catchを使用する場合、余程気をつけないとメモリリークするのではないか
という懸念です。たとえば以下のような場合ですが…
void func1()
{
char *p = new char[ 100 ];
func2(); // func2を呼ぶ
delete [] p;
}
void func2()
{
char *p = new char[ 100 ];
func3(); // func3を呼ぶ
delete [] p;
}
void func3()
{
throw -1; // ここで投げた例外は途中の呼び出しを跨いで
} // main関数内でcatchされる。
void main()
{
try {
func1()
} catch( int err ) {
// リカバリー処理
;
}
// これ以降も処理は続く…
}
プログラムとしてショボイとか無意味というのは別にして、
main関数の中のcatchで何らかのリカバリー処理をし、
その後、何事もなかったかのように処理が進んだ場合、
func1およびfunc2関数の中で動的に確保した領域はリークを起こしたまま、
処理が進んでしまうと思うのですが、
これは「設計が悪い」ということで根本から見直さなければならないのでしょうか?
つまりこのようには組まないよう意識的に気をつけるというか…。
呼び出し階層の途中では、catchされない限り階層を跨いで
いくつもの呼び出し元を遡っていきますよね。
その過程において動的に確保されたメモリをすべて開放しなければならないというのは
結構難しいような気がするのです。
また、従来のプログラムのように、処理結果(戻り値や引数の内容)を見てエラーと
するなら、エラーメッセージなども関数呼出しごとに細かく決められますが、
ある程度大きくまとめられた処理単位をtry ~ catchでくくると、
きめ細かいエラーメッセージは作れませんよね?
そこで、例外を投げる関数を呼び出す場合に、一関数の呼出しごとにtry ~ catch
で括るということになりそうなのですが、それもいまひとつな感じがしてしまうのです。
まぁ、クラスのコンストラクタで発生するエラーなどの場合は、
従来のエラー処理ではどうにもならないので、例外を投げるしか無いとは思いますが、
そもそもコンストラクタで例外の発生するような処理を入れるのが良いのかどうかも
今の自分にはよくわからないところです。(^^;
…ということで割り込み大変失礼いたしました。m(__)m
> そこで、例外を投げる関数を呼び出す場合に、一関数の呼出しごとにtry ~ catch
> で括るということになりそうなのですが、それもいまひとつな感じがしてしまうので
す。
「いまひとつな感じがする」という部分の補足ですが、たとえば1つの「関連が深い
処理」を行うにあたって、以下のような複数の関数を呼び出すものとします。
実際にこのように作ることはまず無いかもしれませんが、具体例ということで…。(^^;
//------------------------------------
// DBから条件に合うデータを取得する
//------------------------------------
InitDb(); // DBアクセスのための各種初期化
CreateCondition(); // DBからデータを取得するための条件を設定
OpenDB(); // DBを使用可能な状態にする
ReadDB(); // DBからデータを読み出す
CloseDB(); // DBの後始末
上記の各関数内で、予期せぬエラーが発生した場合に、
int型の例外を投げるものとすると以下のような感じになると思います。
//--------------------------------------------------
// ■まとめてtry ~ catchでくくると…。
//--------------------------------------------------
try {
InitDb();
CreateCondition();
OpenDB();
ReadDB();
CloseDB();
} catch( int err ) {
// どこで例外が発生したかわからないので、大雑把なエラーメッセージになる
cout << データベースから情報を読み取る際に致命的なエラーが発生 << endl;
}
//--------------------------------------------------
// ■一つずつくくると、エラー処理は細かくできますが、
// かなり見た目に煩雑…。
//--------------------------------------------------
try {
InitDb();
} catch( int err ) {
cout << データベースの初期化で致命的なエラーが発生 << endl;
}
try {
CreateCondition();
} catch( int err ) {
cout << データベースに要求する条件の作成時に致命的なエラーが発生 << endl;
}
//--------------------------------------------------
// ■例外を投げない従来型の関数だった場合
// int型の戻り値を返すものとし、戻り値判定によるエラー処理
// 書式が割りとシンプルできめ細かい後処理も可能(冗長なので内容は省略)
//--------------------------------------------------
if (! InitDB() )
{
;
}
if (! CreateCondition() )
{
;
}
もし呼び出される関数が例外を投げるという仕様なら、try ~ catchで受けなければ
ならないのでしょうが、仕様的に例外を投げなくてすむなら、従来の処理のほうが
シンプルになるような気がするのですが…。
> char *p = new char[ 100 ];
> func3(); // func3を呼ぶ
> delete [] p;
boost::scoped_ptr とか boost::scoped_array とかで解決すると簡単です。
# 標準 C++ に入っていないのが信じられない。
# 次期 C++ には入るらしいですが。
上記 DB 系の処理について言えばおっしゃるとおり try/catch で解決すべきで
ない典型例でしょう。単純エラーに対して例外送出するのは良い設計とは言えな
いです。
try/catch とよく較べられる(類似の機能)として
abort() goto setjmp/longjmp が挙げられると思いますが
abort() : 正しい後始末の機会が無い
goto : 当該関数外へは出られない
setjmp/longjmp : 実装によっては巻き戻しがされない
try/catch : 巻き戻しが保証され、後始末が可能で、どこからでも戻れる
調べるとわかると思いますが C++ 標準ライブラリが例外を投げる状況はかなり
限定されていて、どれも「かなりヤバイ」ないしは「限りなくバグと思われる」
ときだけです。 try/catch が役に立つのは、深い階層から一気に上位階層まで
戻る(戻らねばならない)時で、そういうときは大抵致命傷があったときです。
> これは「設計が悪い」ということで根本から見直さなければならないのでしょうか?
> つまりこのようには組まないよう意識的に気をつけるというか…。
例外を使ったエラー処理を設計に組み込んだ時点で、
このような「関数の末尾で後始末する」というのを全面的に
排除するように気を付ける必要があると思います。
具体的には、初期化と後始末が必要なデータ構造は
classにし、コンストラクタとデストラクタを使用することになります。
動的に確保した配列については、tetrapod さんの仰る通りboostの
スマートポインタを使ってもよいですし、std::vectorとかstd::auto_ptr
でもなんとかなります。
> 上記の各関数内で、予期せぬエラーが発生した場合に、
> int型の例外を投げるものとすると以下のような感じになると思います。
こういうのは例外クラスの設計でカバーできるのです。
DbException
+ DbSystemException
+ DbInitializeException
+ DbOpenException
...
+ DbTableException
+ DbSqlSyntaxException
+ DbQueryException
...
例えばこんな風に階層を作っておけば、
#思いつきなので雑です(^^;
大雑把に全部エラー処理したい場合…
try {
// 一連のDB処理
} catch (const DbExceptino& e) {
cerr << e << endl; // eをストリームに書き出すとメッセージが出る
}
細かくエラー処理したい場合…
try {
// 一連のDB処理
} catch (const DbInitializeException& e) {
} catch (const DbOpenExceptin& e) {
} catch ... {
}
DBそのものエラーとテーブル操作のエラーだけ分けたい場合…
try {
// 一連のDB処理
} catch (const DbSystemException& e) {
} catch (const DbTableExceptin& e) {
}
と、いろんな要望に対応でき、
シンプルになる思いますがいかがでしょうか。
また、例外クラスを作っておくことにより、
エラーそのものに対する処理(エラーメッセージやコードの取得など)
を例外クラス側に持たせることができます。
tetrapodさん、ご返事ありがとうございます。(^^)
いつもお世話になっております。
> # 次期 C++ には入るらしいですが。
な、なんと! boostって次期C++に入る予定があるんですか…。
よくアドバイスなどでも見かけますし、非常に良さげなライブラリなのですが
「標準ではないから仕事で使うのは躊躇われる」と考えておりました。(^^;
> try/catch : 巻き戻しが保証され、後始末が可能で、どこからでも戻れる
これまであったジャンプ系(?)の命令や関数に比べるとかなり融通が利く物
だったんですね。
私が挙げた例でもかなり無茶をすれば(?)解決出来なくもなさそうですし…。
つまりnewを使用した関数内から、例外を投げる関数の呼び出しをすべて
try ~ catchで囲み、catchの中で漏れなくdeleteすれば
確かに後始末は可能かなと…。(^^;;;
> 調べるとわかると思いますが C++ 標準ライブラリが例外を投げる状況はかなり
> 限定されていて、どれも「かなりヤバイ」ないしは「限りなくバグと思われる」
> ときだけです。
やはり本当の意味で「例外的に致命傷」というのを補足する仕組みなのかも
しれませんね。(^^)
単純なエラーでも積極的にtry ~ catchを取り入れるべきなのか
非常に悩んでいましたし、try ~ catchと通常のエラー処理を混在させるのが
良いのかどうか疑問に感じておりましたので、とてもすっきりいたしました。
横からの割り込みでしたが、ありがとうございました。m(__)m
dairygoodsさん、ご返事ありがとうございます。(^^)
> スマートポインタを使ってもよいですし、std::vectorとかstd::auto_ptr
> でもなんとかなります。
tetrapodさんへのご返事の時に書き忘れたのですが、以前似たような質問を
したときにやはり同じようなアドバイスを頂いたことを思い出しました。(^^;
当時はいまいちSTLがわかっていなかったのですが、要は「関数の末尾で
後始末をする」という考え方から「スコープを抜けたときに後始末をする」
というように切り替えれば良かったんですね。(^^)
> DbException
> + DbSystemException
> + DbInitializeException
> + DbOpenException
> ...
な、なるほど…。投げられる例外オブジェクトを階層化(派生)し、
必要に応じてcatch側で使いわけるという事でよろしいでしょうか?
この場合、単純なデータ型を投げるのと違って、例えばクラス内にエラー
コードとエラーメッセージなども含めることが出来そうですが、
一つ疑問に思うのは「例外を投げた関数内でローカルに作られたオブジェクト」
を「例外をcatchする関数で受け取る」場合の問題がいろいろありそうな気が
したのです…。(^^;
つまり、例外を投げる関数から例外をcatchする関数へ渡される例外オブジェクトは、
単純なコピーの形で渡されるとすると、コピーされるということを前提に
作らなければならないと考えればよろしいですかね?
例外オブジェクトの中で「エラーメッセージ」のような文字列を
設定する場合(生のポインタを使う場合?)には、コピーコンストラクタなどの
設定を忘れると困ったことになりそうな気がするというか…。(^^;
これも生のポインタなどを使わずに、コピー可能な文字列クラスなどを使えば
問題ないのかな?(^^;;;
> エラーそのものに対する処理(エラーメッセージやコードの取得など)
> を例外クラス側に持たせることができます。
この部分を読み落として冗長な書き方をしておりました。すみません。(^^;
> try {
> // 一連のDB処理
> } catch (const DbExceptino& e) {
> cerr << e << endl; // eをストリームに書き出すとメッセージが出る
> }
あっ、constの参照型で受け取ってるところを見ると「コピーすることを前提に」
ではないですね…。(^^;
う~ん、例外の発生した関数内で自動変数(自動オブジェクト?)として
例外オブジェクトを定義した場合は、スコープから抜けたらまずいような気が
するので、この辺は何か定石があるのでしょうか?
staticにするとも考えにくいし、グローバルな例外オブジェクトに値を設定して
それを使いまわすのかな?(^^;;; あるいは引数として上から渡していくとか?(^^;;;
> う~ん、例外の発生した関数内で自動変数(自動オブジェクト?)として
> 例外オブジェクトを定義した場合は、スコープから抜けたらまずいような気が
> するので、この辺は何か定石があるのでしょうか?
関数内で生成された例外オブジェクトは、
例外機構内にコピーされますので、コピーされることは前提となります。
catch節でconst参照で受け取っているのは、
派生クラスもまとめて受け取るために必要です。
こちらが参考になると思います。
http://www.fides.dti.ne.jp/~oka-t/cpplab-exception.html
ども、一晩席をはずしている間にたくさんのコメントありがとうございます。
dairygoodsさん:
> catchして為すべき事がある場合だけcatchします。
> 例外機構というのは、エラー処理を一ヵ所にまとめるための
> 仕組みだと思います。
関数階層の深浅に関わらず、逐次処理のどこで発生したかに関わらず、
発生した例外をまとめて面倒見るときだけに catch する、ということですね。
> DbException
> + DbSystemException
> + DbInitializeException
> + DbOpenException
こーいうふうに例外クラスを階層的に設計し、たくさん定義するというのは
まだ使ったことありません。本格的っぽいので、やってみたい気もしますが、
手間だし、ヘッダファイル増えるし……。
> 関数内で生成された例外オブジェクトは、
> 例外機構内にコピーされますので、コピーされることは前提となります。
これは知りませんでした。
確かに throw したら関数からは出てしまうのだから、定義されたブロック
から出てしまいますもんね。
?、ということは、データメンバを持つ例外クラスを自分で定義した場合は
コピーコンストラクタもつけなければ駄目?
tetrapodさん:
> 上記 DB 系の処理について言えばおっしゃるとおり try/catch で解決すべきで
> ない典型例でしょう。単純エラーに対して例外送出するのは良い設計とは言えな
> いです。
> 調べるとわかると思いますが C++ 標準ライブラリが例外を投げる状況はかなり
> 限定されていて、どれも「かなりヤバイ」ないしは「限りなくバグと思われる」
> ときだけです。 try/catch が役に立つのは、深い階層から一気に上位階層まで
> 戻る(戻らねばならない)時で、そういうときは大抵致命傷があったときです。
エラー処理に try/catch を使うかどうかをそもそも最初に判断すべきだと。
覚えたてのときは、まとめて面倒見るのが画期的に見えて、必要ないもの
にまでついつい try/catch 使ってしまいそうです。
とま吉さん:
やり取りどうもでした。
話に広がりが出て面白かったです。
ちなみにMFCでは、
try
{
// Do something to throw a file exception.
}
catch( CFileException* theException )
{
if( theException->m_cause == CFileException::fileNotFound )
TRACE( File not found\n );
theException->Delete(); // <- ウザい
}
↑ってった具合に、ポインタで catchすることを推奨してるみたいですが、
Delete()呼び出すのがウザいですね。ツネに参照で受けたいとこであります。
throw するときは、いちいち変数定義しないで、
throw CXXXExcetption(arg1, arg2);
とやるのが普通なんでしょうね。
(とりとめなくてすみません)
>↑ってった具合に、ポインタで catchすることを推奨してるみたいですが、
推奨しているのではなくて過去との互換性の為にそうなっているものがあるのでしょう
VC++5.0, VC++6.0 は独自の例外処理から C++ の例外への移行期でしたから
新しいクラスでは参照で catch 出来るように設計しているものもあったような気が...
少し長くなってしまいますが、確かめるために以下のように簡単なサンプルを
書いてみました。ここで「注1」となっているコメント部分ですが、
関数内でローカルに定義したオブジェクトで、これをcatchする際には参照で
受け取っています。
これがたまたまうまく行っているのか、それとも例外機構として問題ないのか
というのが心配になっていたりします。(^^;
(つまり関数内でローカルに定義されるオブジェクトへの参照というのが、
なんとなく引っかかる。(^^;)
また「注2」に書きましたが、例外クラスが階層的になっている場合、
派生クラス側から先にcatchしないとダメなようで、
基本クラスのcatchを先に書くと、派生クラスのオブジェクトが投げられたときに、
すべて基本クラスのcatchでcatchしてしまうことがわかりました。(^^;
#include <iostream>
using namespace std;
// 基底エラークラス
class Exception
{
public:
string msg;
Exception(){};
Exception( const Exception &A )
{ msg = A.msg; }
};
// 派生エラークラス
class DerivedException : public Exception
{
public:
string msg;
DerivedException(){};
DerivedException( const DerivedException &A )
{ msg = A.msg; }
};
// 基本エラークラスを投げる関数
void func1( int cnt )
{
if ( cnt == 0 ) {
Exception a; // 注1)ローカルで定義している自動変数
a.msg = Exceptionのメッセージ;
throw a;
}
}
// 派生エラークラスを投げる関数
void func2( int cnt )
{
if ( cnt == 1 ) {
DerivedException b; // 注1)ローカルで定義している自動変数
b.msg = DerivedExceptionのメッセージ;
throw b;
}
}
void main()
{
for( int i = 0; i < 2; i++ )
{
try {
func1( i ); // 初回のループで例外を投げる
func2( i ); // 二回目のループで例外を投げる
// 注2)派生から先にcatchしないとダメ
} catch( const DerivedException &err ) {
cout << err.msg.c_str() << endl;
// 基本クラス型オブジェクトのcatch
} catch( const Exception &err ) {
cout << err.msg.c_str() << endl;
}
}
}
> こーいうふうに例外クラスを階層的に設計し、たくさん定義するというのは
> まだ使ったことありません。本格的っぽいので、やってみたい気もしますが、
> 手間だし、ヘッダファイル増えるし……。
一つのヘッダに書けば問題ないです。
しかし、実際には私自身もここまで階層分けしたことはありません。
クラスを分けるのはエラー処理を分けたいからですが、
エラー処理って殆どの場合、エラーメッセージを出して処理を中断するだけですし…。
例外を使って処理分岐なんてのに凝りだすと、
それは例外処理ではなくて単なる正常系の分岐処理になってしまいますので、
本末転倒しないように注意が必要です(^^;