Home / ぼやきごと / 2009-03-08 / 例外処理とアサート処理
例外処理とアサート処理

例外処理とアサート処理

先に言い訳を書いておきますが、今回の記事はエラー・例外処理に対する私なりの考え方です。
「世間一般でこう定まっている」というわけではありませんので、鵜呑みにだけはしないでください。

さて、C++やC#を始めとするオブジェクト指向言語には、大抵は「例外処理」が実装されています。
例外処理というのは、まぁ要するにtry-catch構文のことです(言語によってはtry-catch-finally)。

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
!
 
-
!
 
-
-
!
!
 
-
|
!
-
// C++
#include <new>
 
// …中略…
 
try
{
    // C++ではnewに失敗するとstd::bad_alloc例外が送出される
    data = new SomeData();
}
catch (const std::bad_alloc& ex)
{
    std::cout << ex.what() << std::endl;
}
// …後略…
すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
!
 
-
!
 
-
-
!
!
 
-
|
!
 
-
-
|
!
// C#
using System;
 
// …中略…
 
try
{
    // C#ではnewに失敗するとOutOfMemoryException例外が送出される
    data = new SomeData();
}
catch (OutOfMemoryException ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    // 例外の有無に関わらず絶対に行わせたい処理を書く
    // 例: 開いたファイルのクローズ
}

一方、C++やC#などでは、アサート処理も提供されています。
C++ではassertマクロ、C#ではSystem.Diagnostics.DebugクラスのAssert静的メソッドです。
これらはデバッグ用にコンパイルされた場合のみ有効になります。

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
-
!
 
-
!
-
!
// C++
#include <cassert>
 
// …中略…
 
// 引数がfalse値だとエラーメッセージを出力してアボートする
assert(data != NULL); // NULLチェック
すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
-
!
 
-
!
-
|
!
// C#
using System.Diagnostics;
 
// …中略…
 
// 引数がfalse値だとエラーメッセージを出力する
// デバッガで実行した場合はブレークポイントと同様に一時停止する
Debug.Assert(data != null); // nullチェック

じゃあどっちを使えばいいんだという話ですが、基本は次の通りです。

  • アサート処理は開発者のデバッグ用であり、ユーザに配布するプログラムには含まれない。
  • 例外処理は開発者のデバッグ用でもあるが、ユーザに配布するプログラムにも含まれる。

じゃあ全て例外処理で書けばいい…というわけではありません。
例えば、次の二つの例を考えてみましょう。

  1. newでメモリを確保できなかった。
  2. プログラム内で閉じている自作関数の引数に不正な値(nullなど)が渡された。

一つ目の例は、どんなに頑張っても発生数をゼロにすることはできません。
たとえ開発者の環境では起こらなくても、ユーザがロースペックなマシンを使っていたり大量のプログラムを同時に走らせたりしていれば起こる可能性があります。
こういった類のエラーはまさに『例外』であり、例外処理でフォローすべきものといえます。
ユーザにメモリが確保できなかった旨のメッセージを表示し、可能な限り状態を保存して終了する…といった処理が妥当でしょうか。

さて一方、二つ目の例はどうでしょうか。
自作関数を使うのは開発者自身ですから、その引数に不正値が渡されるのは開発者のコーディングミス…即ち『バグ』に他なりません。
これに対して例外処理を適用し、ユーザに「○○関数に不正値が渡された」というメッセージを出したとしても、ユーザは困惑するしかないでしょう。

二つ目の例のような「デバッグで完全に除去できるもの」は単なる『バグ』であり、『例外』ではありません。
これに対しては関数の先頭などにアサート処理を設け、デバッグを行うことでバグを除去すべきでしょう。
今時のデバッガならばアサート時にコールスタック(要するに関数呼び出し履歴)を参照できるので、デバッグには便利です。
更に、ユーザ向けのリリースビルド時にはアサート処理は「無いもの」として扱われるため、わざわざコメントアウトしたりする必要もありません。

例外処理は便利なので、ついついそこら中にtry-catchを付けたくなる衝動に駆られることもあります。
ですが、「例外処理はユーザにも見える」ということを常に考え、状況に応じて適切に使うべきだと言えるでしょう。

…ただ、C#やJavaで使えるfinallyは別です。
try-finallyは後処理の必要な場面では例外安全のためにどんどん使うべきだと思います。
特にC#やJavaでは、いつ例外が送出されるかわかりませんから…。

補足

上述の二つ目の例で「プログラム内で閉じている自作関数」とわざわざ書いたのは、ライブラリ等ではまた事情が異なるためです。

一般的に、ライブラリ化する必要のある関数を使うのは自分だけではありません。
それが開発者向けに配布するようなライブラリであれば尚更です。
こういった場合、引数に不正値が渡されるのは十分にありうることです。

そこで例外処理を使うべきかアサート処理を使うべきか…ですが、これはライブラリのポリシーに依ると思います。
例えばソースコードごと公開しているようなライブラリならアサート処理でもいいと思いますが、リリースビルド版しか配布しないようなライブラリではそもそもアサート処理が使えません。
もっとも、引数に不正値が渡された程度のことであれば、関数の戻り値としてfalseなりエラーコードなりを返すという選択肢もありますが(むしろこれが最も一般的か…)。

C++の場合はもうちょっと複雑で、リリースビルド版しか配布しない場合、逆に例外を送出するのは避けた方が良いかもしれません。
というのも、例えば「Visual StudioでビルドされたライブラリをBorland C++ Builderで使う」といったことが往々にしてあるわけですが、C++ではコンパイラによって例外処理の機構が異なっているため、ライブラリから送出された例外をキャッチできない可能性が高いのです*1
C++では例外処理はモジュール(ライブラリ、プログラム)内で閉じている方が無難でしょう。

Category: [プログラミング][C++][C#] - 2009-03-08 09:07:35

  • んー…自作関数で例外を使うべきでない理由としては弱い気が。バグが原因なら例外だろうがアサートだろうがユーザーに見えるべきでないのは同じ。むしろ、万一出荷したコードにバグが残っていた場合、例外ならキャッチしてメッセージを変更(「開発元に連絡してください」とか)できるところ、アサートでは意味不明なメッセージが出るか、しれっと続行して変な結果が出る可能性も無きにしも非ず。 -- aetos 2009-08-05 (水) 11:34:38
  • うまいこと言い返せないものかとGoogle先生に尋ねたらうまいこと言ってる人が居たのでURL貼っておきます → http://ml.tietew.jp/cppll/cppll/thread_articles/12077#ar12086 。…でもC#やJavaみたいに例外処理がきちんと整備されている環境なら例外オンリーでもいいかなと思ったり。確かC#の例外クラスは発生時のコールスタックを持っていた気がしますし。 -- ルーチェ 2009-08-07 (金) 01:17:49
  • 私の場合は仕事で製品を作ることが主で、バグはチェッカーを動員して全て潰すことが前提なので、チェッカーの方に「プログラムが落ちました」と言われた時、すぐCOREファイルをgdbにかけてデバッグすることのできるassertには魅力を感じます。…まぁ、それでも全部は潰せないことも事実ですが。 -- ルーチェ 2009-08-07 (金) 01:28:52
  • リンク先には「腐った製品です」とありますが、もし製品にバグがあったとき、落ちるのと落ちずに動いて予期しない結果を生むのとではどちらが腐った製品でしょうか。バグがある時点で腐っていると言えばそうかもしれませんが、それはassertを使うか例外を使うかとは関係のない話です。また、「どこかで誰かがstd::exceptionをキャッチしているとデバッグしようがありません」の話は、それこそそんな作りになっているのはバグがある以上に腐った製品です。また、パフォーマンスも問題になりません。例外の throw は遅いですが、滅多に起こらないことなので気にしません。try を仕掛けるだけで遅くなる言語もありますが、バグに起因する予期しないエラーのハンドラは呼び出し階層のトップに1つだけ設ければいいだけなので、無視できる程度のものです。コアダンプについては、そういうデバッグをしたことがないのでコメントできませんが、特定の言語の特定の実装に依存する話で、この記事のコメントとしては不適切な印象を受けます。 -- aetos 2009-08-07 (金) 12:45:43
  • まず、この記事を自分自身で読み返してみて、確かに私のコメントは二つとも方向がずれていると感じました。その点は申し訳ありません。 ずれていることを認識した上でリンク先の意見を勝手に擁護させてもらうと、「バグがある時点で腐っている」ことが「assertを使うか例外を使うかとは関係のない話」と言い切るのはどうかと思います。バグがある時点で腐っているからこそ、即ちそんな腐ったバグを潰したいからこそ、assertを使うのではないでしょうか。リンク先はC++の話題であり、C++(の多くの環境)ではコールスタックを参照できるという点でassertにデバッグ面での優位性があります。確かに製品でもlogic_errorが起きる可能性は捨て切れないでしょうが、それ以前に開発途中ではより高い確率で起きるわけで、その時にデバッグしやすいassertを選択することは決しておかしくはないと思います。…以上の話はC++に限った話です。C#やJava等ではアサート処理の優位性は無いのかもしれません。そういう言語であれば、上のコメントにも書いた通り例外処理オンリーでもいいのではないかと思います(結局特定言語に依存した話になってしまっていますが、そもそも例外処理とアサート処理自体が各言語や環境ごとの実装に依存するので仕方ない気もします)。 -- ルーチェ 2009-08-08 (土) 12:52:16
  • なんかもう(私のせいで、私の)話がずれにずれてしまいました…。「privateな関数の引数チェックにまで例外処理を持ち出して、まずあり得ないはずの例外発生時の処理まで記述するなんて仰々しすぎるし、コスト対効果も悪すぎる。…けど、まったくチェックしないよりはassertの方が良くね?」といった感じで書いた記事だったと思うのですが…(publicな関数は話が別ですよ)。 あと、「どこかで誰かがstd::exceptionをキャッチしているとデバッグしようがありません」の話は…、そんなことをしない完璧なプログラマとライブラリしかこの世に存在しないなら、リンク先の方もそんな心配をすることはなかったんじゃないかな…。 -- ルーチェ 2009-08-08 (土) 12:55:02
  • C++におけるassertの優位性は、おっしゃる通りかもしれません。俺はC++で大規模なアプリを組んだことがないので、その優位性がわかりません。その点はノーコメントとさせてください。なお、C#では例外でもスタックトレースが取得できます。Javaはどうだか知りません。リンク先の「腐ったアプリ」発言は、比較の軸がずれている気がします。logic_errorを捕まえて「開発者に連絡してください」というメッセージが出るのは、リリースしたアプリにバグがあるという前提です。が、その前段階で、リリースしたアプリにバグがあってはならないとあり、その上でlogic_errorのみに言及するのは不公平な比較です。それ以上はassertの優位性の話になりますので、やはりコメントを控えさせて頂きます。「まずあり得ないはずの例外発生時の処理まで記述するなんて仰々しすぎるし、コスト対効果も悪すぎる」には若干の反論をば。例外発生時の処理は前述の通り、mainに1回だけ書けばよいもので、その目的は、例外メッセージを「開発者に連絡してください」に差し替える程度のものです。コスト的にはほぼ無視出来るのではないかと思います。さて、今更ですが、俺が言いたいのは「例外を使うべき」でも「assertは使うな」でもなく「例外を使ってはいけない理由として弱い」ということです。 -- aetos 2009-08-10 (月) 11:55:41
  • 「mainに1回だけ書けばよい」で済まないのがC++(のライブラリ)。補足参照。まぁそもそもC++は例外処理が完成されていませんから、C++ベースで話をする時点でアンフェアだったかも。 …さて、「例外を使ってはいけない理由として弱い」については正直返す言葉がありません(それで他人の意見に頼った結果がこれだよ!)。ただ、本文にもある「バグはバグであり例外ではない」という私個人の考え方でいくと、privateな関数の引数チェックなどで例外をthrowすることにはやはり抵抗を覚えます。privateな関数は同じモジュール内のpublicな関数からのみ呼ばれているわけで、そのpublicな関数は外から見えるわけですからもちろん適切な例外処理が施されているべき/はずで、その結果privateな関数は正しい状態・引数でしか呼び出されないはずなのです。ところがそうなっていないとしたら、それは明らかにコーディングミスが原因の『バグ』なわけで、状況・環境によって起きうる『例外』とは全く異なるものだと思います。これを例外としてthrowしてしまうのは、例外処理を必要以上に濫用している気がします。…まとめると、「アサート処理を使えとは言わないが、例外処理を使うことには違和感を感じる」といったところでしょうか。 -- ルーチェ@実家 2009-08-13 (木) 03:46:55

*1 昔見聞きした記憶に頼って書いているため、現在は事情が異なるかもしれませんが。