皆さんこんにちは、高橋と申します。
開発環境 Win XP(SP1), Visual C .NET 2003, コンソールアプリ, C++標準のみ使用
最近例外処理を自作クラスに組み込んでエレガントなエラー処理というのを
目指して勉強中なのですが、どうも例外に関して何か勘違いしているような気がしてな
りません。
試しに、以下のようなプログラムを試作してみました。
// hoge が投げる例外クラス
class ex_A {};
void hoge1()
{
// 何らかの処理の結果 ex_A がスローされたとする
throw ex_A();
}
void hoge2()
{
static int iRetryCount = 0;
try{
hoge1();
}
catch(ex_A& e){
// 3回 リトライした?
if(iRetryCount ++ > 2){
cout << 3回リトライに失敗したので例外をスロー << endl;
throw;
}
// リトライ
cout << リトライを試みる : << iRetryCount << endl;
hoge2();
}
iRetryCount = 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
try{
hoge2();
}
catch(ex_A&){
cout << hoge2 は例外 x_A のリトライに失敗 << endl;
}
getch();
return 0;
}
*プログラムの概要
(1) hoge1 は 例外 ex_A をスローする可能性がある関数です。
(2) hoge2 は、hoge1 が 例外 ex_A で失敗した場合 3 回だけリトライします。
(3) 最終的に3回ともした場合は hoge2 の呼び出し側 main へ ex_A をスローします。
例えば hoge1 が何らかの通信する関数(LAN, RS-232C等で受信など)だとして、
受信待ちタイムアウトの例外として ex_A をスローするケースを想定し、
さらに1回失敗してもリトライして通信を試みるという事をやりたかったからなのです
が、
catch ブロックの中から再起呼び出しをかけるというのが何かスマートではないという
か、
何か無理をして例外を使っている様に思え、取り返しのつかない方向に突っ走っている
のでは
と気になってしょうがありません・・・。
# 「素直に戻り値返してエラー処理したら?」という感じが濃厚・・・
このような例外の使用方法が正しい(一般的)のか、どなたかご意見を頂けませんでしょ
うか?
よろしくお願いいたします。
例外は'起きてはならない非常事態が発生した'ときに使うもので、
通信以上はむしろ日常茶飯事、折り込み済みの状態ちゃいますか?
だったらフツーに戻り値でやった方が。
この例で行けば、3回目の失敗で初めてthrowとか。
っく orz
x: 通信以上
o: 通信異常
> この例で行けば、3回目の失敗で初めてthrowとか。
御意。
仮に、既存 hoge1()がタイムアウト例外を投げてしまい
修正できないなど仕方がない場合でも、
再帰用の static があると同時呼び出し出来なくなって
タイムアウトするような処理には無かなそうな気もしますし、
hoge2は 無難に hoge1 を含むtryブロックをループで
呼び出した方がいいのではないかと思います。
そですね。
親へどんどんさかのぼって行くというのも
「俺の手にゃ負えんからどうにかしてくれ、頼む」
って投げちゃってるってことですから。
もうどうしようもない時だけ使うべきかと。
個人的には、想定外の事態が発生したときに投げるものだと思います。
ただ、エラーケースをどこまで想定するかは判断が分かれるところですが。
俺はかなり想定を絞る方です(前提条件をキツくする)。
bool hoge1(){
return .....;
}
void hoge2(){
for( size_t i = 0 ; i < 3 ; i++){
if( hoge1()) return;
}
throw ex_A();
}
だね、hoge1() 失敗することが前提っぽいんで、戻り値で返すべきでしょう。
さすがに3回失敗するのは変だろう、ということでhoge2()は例外を投げる、と。
それと、同じ例外を違う意図で使うのもなんか変な感じがする。
hoge1 が例外を投げるにしても、hoge2 の例外とは違うもののほうが意図が明確になるし、
繰り返すために再帰するのもちょっと変。
皆さんたくさんのレスありがとうございました。
こんなくだらないスレッドにつきあって頂いて感激です!
επιστημη さんレスありがとうございます。
> 例外は'起きてはならない非常事態が発生した'ときに使うもので、
確かに仰るとおりですね・・・
投げるにしても3回目で、例外をスローした方がスマートですね。
# ただ、それすら投げる必要があるのかという気がしてきました・・・。
何か例外というと、戻り値で正常かどうか判断しなくても良い
というイメージが私の場合強くて今回のようなプログラムを
書いてしまったわけですが、参考書にも例外をエラー(異常処理?)
と割り切って使用するべきだと確かに書いてました。
この辺はかなり理解不足でした。例外処理を万能なエラー処理と思いこんでいたようで
す。(お恥ずかしい・・・)
この場合、受信に失敗するのはエラーではなく、単なる一つの状態
ん・・なんて言って良いのか・・・受信できても、できなくても
それは致命的な問題ではなく、普通にあり得る状態だと。
そいうことならば、それは例外ではなく戻り値(例えば bool)
で返せば良い問題だと言うことなんですね。
納得しました。
Ban さんレスありがとうございます。
> 再帰用の static があると同時呼び出し出来なくなって
うは・・・気づかなかった。実際には使う気はない関数(テスト用なので)ですが
ここまで気づきませんでした。
万が一使ってたらはまるケースもあるんだろうなと勉強になりました。
シャノンさんレスありがとうございます。
> ただ、エラーケースをどこまで想定するかは判断が分かれるところですが。
> 俺はかなり想定を絞る方です(前提条件をキツくする)。
今回のケースは、私の判断ミス(というかあまい)ということですね。
絞り込みというのもなかなか難しいですね・・・。
きちんとクラスなりの設計を行って返すべき例外を最小にする。
つまり、致命的なエラーとは何か?を追求する必要があるのだと思いました。
職業プログラマーの方には、当然のことなのでしょうが、私の場合日曜大工的な
プログラマーなものでして(テストプログラムとかツールの類を良く作っています)、
まともに設計などしたことがありませんでしたが、プログラムを書くには
やはり設計が(仕様検討)必要なことなのだなと痛感致しました。
PAI さんレスありがとうございます。
提示して頂いたプログラムを見て、やはり hoge1 は、戻り値を返すべきだと
改めて思いました。
# どう考えてもこっちの方がすっきりですし・・・ (^_^;
> それと、同じ例外を違う意図で使うのもなんか変な感じがする。
確かに、このケースだとどっちが投げたのかよくわからないし、
適切ではないと思いました。また、やはり再帰は変ですね・・・
全然エレガントじゃないです・・・
皆さんのおかげで何か例外処理に対して誤解が解けたような気がします。
bad_alloc のようなかなり致命的な例外ならまだしも、普段頻繁に起こるような
もの(今回のケースのような)に対して例外を使用するのは完全にお門違いで、
例外とはプログラムを停止するかどうかなくらい致命的な問題
または、例外を使用することによって単純にプログラムが書ける場合に(こっちは怪し
いですが)
に使用するべきでだということですね。
なかなか、例外を組み込むと言っても難しいですね・・・よく考えてみます。
ですが、かなりすっきり致しました。皆様、どうもありがとうございました。
# これで解決とさせて頂きますが、もし私がまだ間違っているようでしたら
# 一言言って頂けると助かります・・・
書き忘れましたが、例外を使用するのは、コンストラクタもですね。
# ただ、MFC の CFile とかバンバン例外投げるし、致命的かどうかは問題じゃないのか
な・・・
Javaなんかだとかなり軽微なエラーでさえも例外投げるし…価値観の相違かもしれません
です。
ただ、なんもかんも例外で済ますってものではない。
例外でしか対応できないケースもあります。
> Javaなんかだとかなり軽微なエラーでさえも例外投げるし…価値観の相違かもしれま
せん
Java は全くわからないのですが、例外をエラー処理のためのメカニズムとして積極的に
用いているわけですね。
επιστημηさんも、皆さんも仰っているとおり C++ の例外と Java とでは考え方が違っ
ているようですね。
よく考えると CFile も、普通に動いている分にはコンストラクタを除き例外は発生しな
いだろうし、CFileException 例外が起こるという事はファイル処理に関して致命的なエ
ラーが発生したということなのかな?
何が致命的で、あるいは致命的ではないのか、クラスの設計者の判断に委ねられるわけ
であって・・・まあ当然といえば当然ですが、やっぱC++って難しいですね。
> 例外でしか対応できないケースもあります。
戻り値を返せない場合例えば、コンストラクタ、デストラクタ、演算子の多重定義くら
いしか思いつきませんが、他にまだあるのでしょうか?
でも、いずれの場合も例外を使わなくともエラー処理できそうですし・・・
うーん・・・、0割とか自分のプログラムに直接関係ないOSのエラー(ハードウェアがい
かれたとか?)のことなんでしょうか?
0割なんて割る前に事前にチェックすればよいし、システムの異常なんてユーザレベル
(少なくとも私レベル)じゃどうしょうもないし・・・
それとも、またもや勘違い!?(の様な気がする・・・あうう)
> ...演算子の多重定義くらいしか思いつきませんが、他にまだあるのでしょうか?
> でも、いずれの場合も例外を使わなくともエラー処理できそうですし・・・
hoge a, b, c;
c = a + b;
さて、a + b の最中にエラーが起こったら、どうやって処理します?
演算子は例外で処理するのが普通だとは思っうのですが、やろうと思えばできなくもな
いなと言うことでまずい発言でした。(すいません)
ちなみに、せっかくお題を頂いたので考えてみました。
class hoge
{
bool m_bErr;
public:
hoge(bool bErr = false) : m_bErr(bErr) {}
hoge operator + (hoge& a){
// エラーが起こったとする
return hoge(true);
}
void operator = (hoge& b){
// b が正常ではない場合
if(b.IsErr()){
m_bErr = true;
return;
}
// 正常な場合 ...
}
bool IsErr() { return m_bErr; }
};
int _tmain(int argc, _TCHAR* argv[])
{
hoge a, b, c;
c = a + b;
if(c.IsErr()){
cout << エラーですよ! << endl;
}
getch();
return 0;
}
もう C++ 使う意味ない位やばめなコードですが、これでどうでしょうか?
こんないずれ破綻することが目に見えてるコード書かなくともすむので例外があるので
すね・・・
というわけで修正致します
例外とはプログラムを停止するかどうかなくらい致命的な問題。または、例外を使用す
ることによって単純にプログラムが書ける場合(コンストラクタ、演算子の多重定義
等)などに使用すると吉。
επιστημη さんご指摘ありがとうございました。
あ、あとデストラクタはオブジェクトが消えてしまっているわけだから、グローバル変
数使うくらいしかチェックのしようがありませんが、複数のクラスがあると破綻する
し、どっちにしろ無理ですね・・・
# すぐに気づけよと思われる・・・とほほ
解決後に失礼!
----------------------------------------------------
例外の投げ方
----------------------------------------------------
例外の発生させ方は3種類有ります。
class EXP{ //例外クラス
};
・実体渡し
throw EXP();
・参照渡し
throw new EXP();
・固定渡し
static EXP exp;
throw exp;
これらは諸説ありますが、「参照渡し」は嫌われているようです。
(catch後にdeleteしなければならないから・・だそうです)
基本的にシステム内で統一することが大前提で、それを守ればどれを使っても問題無いです。
但し、マルチスレッドかつ例外をクリティカル以外で使うシステムでは「参照渡し」です。
(ちなみに私は参照渡しを良くチョイスします)
----------------------------------------------------
例外のカテゴリ
----------------------------------------------------
次に例外の種類ですが、、
私はいつも基本例外クラスから3種の例外を派生させます。
class EXP_BASE{};
class EXP_WARNING:public EXP_BASE{};// 警告
class EXP_ERROR:public EXP_BASE{};// エラー
class EXP_FATAL:public EXP_BASE{};// クリティカル
更に、スレッドハンドラ関数のトップで(つまりスレッドの一番上の関数)
catch(EXP_BASE *){}
として、catch漏れを防ぎます。
私は参照渡しモデルを良く使う為、例外クラスのサイズは極力小さくします。
(致命的例外でnewが成功する確率を考慮しています。私は最低でも100byte以下にします)
----------------------------------------------------
注意点
----------------------------------------------------
致命的以外で例外を使う場合、いくつか注意点があります。
--------------------------------
コンストラクタから例外を投げない!
--------------------------------
コンストラクタでは既にクラス用のメモリが確保されていますので、その領域を開放できませ
ん。
(回避する方法も有りますが・・・一般的ではありません
参考までにoperator new(void*,size_t)です。)
--------------------------------
自動処理を徹底する!
--------------------------------
・領域管理
・排他管理
・状態遷移(リスト等の整合性やフラグ管理)
これらは、明示的な後処理を通らない事を考慮しなければなりません。
これらは例外発生時のスコープ内スタックのロールバック処理
(アンワインド・セマンテックと言います。コンパイラオプションで有効・無効を選択できます)
を利用して解決します。
スタック上に管理用クラスを宣言し、そのデストラクタで処理後処理を行います。
・領域(ヒープ)管理ならばスマートポインタ等を利用してコード上に明示的なdeleteを書かな
いようにします。
・以下、排他、状態遷移等の例
例えばクリティカルセクションならばこうです。
class crs_mng{ //
CriticalSection *CS;
bool lock_flg;
public:
mng(CriticalSection *cs){CS = cs; lock_flg = false;} // クリティカルセクション実
体をもらう
void lock(){ EnterCriticalSection(CS); lock_flg = true;} // 排他開始
void unlock(){ if(lock_flg)LeaveCriticalSection(CS); lock_flg = false;} // 排他
中なら解除
~mng(){ unlock(); } //自動的に開放
}
CRITICAL_SECTION g_cs;//イニシャライズ済み
int hoge_func(){
{
mng m(&g_cs);
m.lock();
:
if(hoge)
throw new EXP_ERROR(); // <-自動的に~mng()が走りunlock()される
:
}
}
以上!
なんか、いつも長いなぁ~~
読み返すと自動処理の部分がわかりにくいので、追記。
int hoge_func2{
init();
:
:
if(hoge){
throw new EXP();
}
:
:
term(); <-ココが飛ばされる場合があります。メモリーや排他を扱っていると危険!
}