いつもお世話になります。
ebimayoです。
今回お聞きしたいことは例外やエラーについてです。
処理を書いていく中で毎回悩むのが例外やエラー処理なので、
その一般的?な考え方をお聞きしたいと思い投稿しました。
#何が一般的かわからないですが、自分が一般的かな?と思うものが知りたいです。
1.例外とエラー(失敗?)の違いとはなんなのでしょうか?
自分の解釈
例外 =処理失敗時にthrowするもの
エラー=処理失敗時に戻り値で異常を知らせるもの
2.自作関数の場合、処理異常時に例外としてthrowするのが望ましいのか?
それともエラーとして戻り値を返すのが望ましいのか?
ケースバイケースだとすると、何をエラーとし何を例外とするのが望ましいのか?
今回お聞きしたい内容はC++でのコーディング方法や考え方について
なので、自分の開発環境ではなくお聞きしたい対象環境を以下記述
Windows98、NT4.0、2000、XP
コンパイラの指定はなし(VSの場合もMFC使用有無も指定なし)
# 「プログラミング言語C++」とか読むと言語設計者の意図とか読み取れるかと。
# JavaとかC#とかのクラスライブラリでどんな例外を使うかも参考になるかと。
一応、私の個人的な意見を。
1.
言語機能的な差異:
例外処理は、戻り値と異なり無視する(catchしない)とabortする(早期発見に役立つ面)
例外処理は、戻り値の伝言リレーをしなくても呼び元まで処理を辿る。
問題点:
例外処理は、モジュールをまたがるとランタイムに依存する。
例外処理は、例外脆弱な処理に問題をもたらす可能性がある。
2.
例外:
・基本的には「ここは対応しない/できない/許容しない/責任範囲外」等の意思を
実装で表明するもの。
例外を投げるのは、自分が対応しない/できない処理や、許容しない処理、
実行環境や別モジュールなどの責任範囲外で失敗した通知など。
・通常発生しない/発生してはいけない処理。
例えばstd::logic_errorとか。そこが呼ばれること自体がおかしいなど。
→ごめん、やっちゃった。無理、ギブアップ。後は頼んだ。
・対策をとらない/とる気がない処理。
正しい前提で処理をし異常系を処理しないなど(std::invalid_argumentとか)
→書式くらいチェックして持ってきてくれないと、こんなん渡されても困るよ。
頻度が低かったり、異常に手間がかかったり、発生時の打つ手が少なかったり、
対策を諦める場合など(std::bad_allocとか)
→これはちょっと無理だわ。一応例外を送るから何かするならがんばって。
・必ずしもエラーとは限らない(が、現実的には大半がエラー)
割り込みや中断、タイムアウトなどは、エラーでなくても例外的と言えることも。
エラー:
・想定しうる処理失敗/異常系処理で、対応の用意/意図/可能性があったもの。
逆に言うと、自分が受け付けて処理を試みた上で完了できなかったもの。
→確かに書式はあってるから受領したけど、これできんわ。
Banさん回答ありがとうございます。
思いっきり個人的な視点ですが、try-catchが好きではありません。
なので、大して使う意味が無ければなるべく戻り値などで判断させたいと
思っていました。
好きでない理由は
1.ネストが深くなる
2.戻り値判定と違って、どの処理(関数)に対しての
異常処理を書いているのか分かりにくく、VBとかのGOTO文的な感じに思えてしまう
(例えばFileのopen,read,seek,write,closeなどの一連の処理を書いたとき
これをtry-catchで囲む場合とか)
この辺は言語仕様(意図)に沿ったコーディングが望ましいという
ことになるのでしょうね。
私もすごく個人的な意見しか書いてないので、
筋が通っていると思えば各人の思いのままでいいとは思いますが、
> 2.戻り値判定と違って、どの処理(関数)に対しての
> 異常処理を書いているのか分かりにくく、VBとかのGOTO文的な感じに思えてしまう
どの関数に対する異常処理とかを区別することなく、
「異常処理」が持つ意味/内容によって異常処理を振り分けるために
あるんじゃないですかね>例外処理。
戻り値で言えば、エラーコードドリブン?とでもいうか。
逆に、全ての関数が、内部処理でメモリ確保に失敗する可能性があり、
同一エラーコード(例えばNOMEM)を返してくる可能性がある、とか。
関数から戻るたびにちまちまエラーチェックしたくない(けどせざるを得ない)と、
うっとうしいと思うこともありますし。
正常系でむやみに多用するものではないものだとは思います。
> (例えばFileのopen,read,seek,write,closeなどの一連の処理を書いたとき
> これをtry-catchで囲む場合とか)
例えばこの例で言うと、
・処理中にユーザの外部操作によりストレージが外された
・処理中の対象ファイルが強制的に外部から更新されてコンフリクトした
とか、こういうのが本来例外処理に適したものだと思いますが。
Fileクラスとしては以後の動作が保証できず、継続もできず、
Fileクラスにはリカバするだけの情報もないので、呼び元に戻して判断を仰ぐくらいし
か。
あとは、呼び元の方で、try内のロールバック/フォワードを試みるなり、諦めるなりす
る。
> 1.ネストが深くなる
俺はむしろ、例外処理を使うとネストが浅くなると思います。
確かに、try ブロックの中に処理を押し込めることで、一段階は深くなります。
しかし、同等のことを if 文でやろうとしたら、二段、三段と深くなるでしょう。
俺がよくやるのが、レジストリキーの操作ですが、これなんか悲惨なもんです。
キーをオープンする
if( 成功した )
{
特定の値の長さを取得する
if( 成功した )
{
値の長さ分のバッファを割り当てる();
if( 成功した )
{
値を取得する
if( 成功した )
{
実際の処理を行う
}
バッファを開放する
}
}
キーをクローズする
}
こんなふうに、if( 成功した ) がたくさん連なるようなのから見ると、例外処理の何と
楽なことか。
Banさん シャノンさんありがとうございます。
返信が遅くなりすいません。
Banさんの例外に対する考え方、確かにそうだなと思うところがあり
参考になります。
シャノンさんの例の場合、私の場合だとよく
以下の処理を関数としてまとめて
キーをオープンする
if( 失敗した ){
return オープン失敗を知らせる戻り値
}
特定の値の長さを取得する
if( 失敗した ){
キーをクローズする
return 値の長さを取得失敗を知らせる戻り値
}
値の長さ分のバッファを割り当てる
if( 失敗した ){
キーをクローズする
return バッファを割り当て失敗を知らせる戻り値
}
値を取得する
if( 失敗した ){
キーをクローズする
バッファを開放する
return エラーを知らせる戻り値
}
実際の処理を行う
という感じにしてしまうことが多いです。
#失敗時の後処理が多い場合はこれをひとまとめにした関数にして処理させてます
本来の正常処理がネストするのを極力避けたく思う思考回路っぽいです。
try-catchだと正常処理をネストさせるのが気になって。。。異常処理がネスト(?)
するのは正常処理と分けれて好きなんですけれど、
この辺は個人差で何が好きか分かれるところなのでしょうね。
いろいろ考え方を教えていただきありがとうございました。
解決していますが…。
私の場合、throw(XXX error)なんていうのをちょくちょく使います。
関数内エラーを、戻り値にし、それを何らかの形で表示させる場合、エラー状況から文字
列を作成してメッセージボックスなりprintfなりで表示するかと思いますが、その場合呼
ぶ人によってエラーメッセージ表現が変わってしまうことがあると思います。(Win32
APIのFormatMessageみたいなのを作ればよいのですが…。)
void test(void)
{
if(エラー)
throw(hoge error);
}
try
{
test();
}
catch(const char *str)
{
puts(str);
}
こんな感じにすれば、エラーメッセージを簡単に表示させられると思います。
参考までに…
> シャノンさんの例の場合、私の場合だとよく
> 以下の処理を関数としてまとめて
[ 中略 ]
> という感じにしてしまうことが多いです。
は、俺だったら(例外処理を使わなければ)こうなります(下記参照)。
で、この方法は、原始的ではあるけれども、例外処理の考え方と基本的に変わりませ
ん。
goto を使うことが望ましくないと考える方もいらっしゃいますが、俺は乱用することな
く、特定の目的のみに使うのならば構わないと思います。
ただし、C++ には例外処理機構があるのですから、goto を使ってそれを再現することも
ないんですが。
キーをオープンする
if( 失敗した ){
return オープン失敗を知らせる戻り値
}
特定の値の長さを取得する
if( 失敗した ){
戻り値変数 = 値の長さを取得失敗を知らせる戻り値
goto クリーンアップ処理1
}
値の長さ分のバッファを割り当てる
if( 失敗した ){
戻り値変数 = バッファを割り当て失敗を知らせる戻り値
goto クリーンアップ処理1
}
値を取得する
if( 失敗した ){
戻り値変数 = エラーを知らせる戻り値
goto クリーンアップ処理2
}
実際の処理を行う
クリーンアップ処理2:
バッファを開放する
クリーンアップ処理1:
キーをクローズする
return 戻り値変数
> 本来の正常処理がネストするのを極力避けたく思う思考回路っぽいです。
ということは、
try
{
処理1
try
{
処理2
try
{
処理3
}
catch
{
例外処理3
}
}
catch
{
例外処理2
}
}
catch
{
例外処理1
}
となる、ということでしょうか?
確かにこれは醜いです。if( 成功した ) の連続と同じ様式ですね。
が、俺は個人的には、先にあったような
try
{
処理1
処理2
処理3
}
catch
{
例外処理
}
というのが好みですね。
NowNowさん シャノンさんありがとうございます。
ケースバイケースなんだとは思うのですが、C++だと異常を例外として
throwすることを駄目出しするようなサイトや意見は目にしたことないですが、
VB6.0とかでエラーをgotoで飛ばすのは分かりにくくなる等の意見を聞いた
ことがあり、同じような用途として使うのに不思議だなと思ったりもしていました。
他サイトやここで話を聞いていると上級者?ほどtry-catchやthrowを
うまく使ってるような感じました。