ぼやきごと/2009-03-08/例外処理とアサート処理 のバックアップ(No.5)


例外処理とアサート処理

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

さて、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

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