行き場のないエラー - エラー処理とassert

失敗はつまずくことではない。つまずいたままでいることだ。

またまた更新間隔が開いてしまったので、久々の更新です(^^;)。今回は「エラー処理」を取り上げてみます。「エラー処理」という単語もあいまいなのですが、今回とりあげるのは「あるメソッド・関数が本来の処理を達成できなかった場合の処理」といったところです。

エラーを伝える

さて、処理が失敗した時の最も基本的なパターンは、戻り値としてエラー値を返すことです。このような関数・メソッドは数多くあります。例えば、POSIX系のclose()関数をみてみましょう。
int close(int fd);
これはエラーで0以外の値が返ります。このように、エラーか否かを戻り値で返すものの他に、本来の戻り値がとり得ない値をエラーの意味として返すものもあります。例えば、POSIX系のopen()関数です。
int open(const char *pathname, int flags, mode_t mode);
この関数は戻り値としてfd(file descriptor)を返しますが、エラー時は-1を返す仕様です。

どちらにせよ、この関数を使用する側としては、戻り値を確認することによって、エラーが起こったか否かを確認することができます。

エラーを処理する

さて、大半の関数がこのようにエラーを戻り値で通知してくれるわけですが、そのようなエラー値を見つけた場合、一体どうすればいいのでしょうか。大抵の場合、次の3種類に分けられるでしょう。
  • 1. エラー処理としてユーザーへの通知や、代替処理、後始末などを行う。
  • 2. エラーを無視する。
  • 3. 処理をそこで止める。

1は最も基本的なパターンです。ファイルがオープンできないといった場合であれば、ファイル名が違う(あるいは存在しない)とか、パーミッションが不適切だとかいう場合がありますので、この場合はユーザー(又は上位モジュール)に通知する必要があるでしょう。場合によっては、ネットワーク越しであればリトライ等も必要かもしれません。書き込み中のエラーならファイルをクローズするなどの後処理も必要でしょう。
しかし、実際にこのようなエラー処理が行われるのはごく一部です。大抵の場合は、2の「無視」に落ちることが多いと思います。

大抵の関数はエラーを返してくれるのですが、実際全ての関数ごとにエラー処理をするのは難しい、あるいは無理がある場合がほとんどです。例えば、バグでしかエラーになりえないものに対して、適切なエラー処理を与えるというのは不可能でしょう(全てのprintfのエラー見てる人なんていませんよね)。それに、全ての関数毎にエラーを処理しようとすると、次のようなすさまじい入れ子ができあがりかねません。
  /* 処理A, B, C, Dを行う */
  if (func_A(x, y) == 0) {
      ... 
      if (func_B(x) == 0) {
          ...
          if (func_C(y) == 0) {
              ...
              if (func_D(x, y) == 0) {
                  ...
open()のようなI/Oのための関数は、途中でエラーが発生する場合もあると思いますが、論理的な関数の場合は、最初の関数が成功すれば、あとは絶対成功するはずという場合もあります。このような場合であれば、確かにエラー処理は必要ありませんし、しようもありません。しかし、エラー処理が必要ないというのと、エラーをチェックしなくて良いというのは違います。つまり、単純に無視していいとは限りません。そこで、3の「処理を止める」というのが登場します。

見てみぬふりをしない

次の例を見て下さい。
...
pthread_mutex_lock(&mutex)
shared_variable ++;
pthread_mutex_unlock(&mutex)
...
いたって単純なmutexでの排他制御コードです。ここでmutex_lockはエラーを返す可能性がありますが、エラーは確認されていません。mutex_lockがエラーになるのは、初期化忘れや、同じスレッドでの二重取得等ですが、自身での本来、このような例でエラーが確認されない理由は、それは外的要因ではなく、単なるバグです(trylockと違い、busyならまたされるだけ)。よって、ユーザーに通知するという必要もありませんし、エラー処理で復帰できる望みも薄そうです。では、どうするか。このコード例では特にエラーを見ずに無視していますが、これでいいのでしょうか。

プログラムにバグがなければ、自スレッドから再度mutexをとってみたり、初期化されていない構造体を渡したりということは発生しません。しかし、開発中にはこのような不正規な処理のために、よくデッドロックを起こしてしまうことがあります。しかも、このようなバグは、タイミング依存になったりして、なかなか発見されません。
しかし、この例の場合、関数(pthread_mutex_lock())のエラーを見ていれば、すぐに分かったはずなのです。

よって、ここは以下のようにすべきでしょう。
...
ret = pthread_mutex_lock(&mutex)
assert(ret == 0);
shared_variable ++;
ret = pthread_mutex_unlock(&mutex)
assert(ret == 0);
...
assertにひっかけてやることで、おかしな動作をした場合、すぐに 検出できます。
バグというのは、発生地点から離れれば離れるほど解析が難しくなりますので、このように発生時点で捕まえるというのは非常に重要です。
なお、標準のassertマクロなら、リリース時に消すこともできまし、検出時に行数も出してくれますので、リリース物の効率にも影響を与えません。

ライブラリの中心でエラーを叫ぶ

さて、上記の例では、エラー値を見て処理を止めていました。しかし、エラーを起こしてはいけないのはここ部分のコードだけではないでしょう。mutex_lockを使用しているコード全般に言えることです。であれば、mutex_lockしている方ではなく、mutex_lockの方でassertにひっかけてやる方がいいのではないかということも考えられます。

その通りです。pthread_mutex_lockの例で言えば、エラーを返す前に、abort()するようなコードを入れておけば、不正な処理が行われた段階でプログラムは停止します。デバッガを繋ぐなりしてあれば、バックトレースでどこが問題かもすぐ解析できます。

しかし、この方法はライブラリのソースコードがない場合はそうはいきません。また、自分がライブラリを作成する側の場合、ライブラリ内で勝手にabortするのは、常に期待値とはいえません。自分がライブラリ作成者である場合は、ライブラリ使用者のために、エラー時の挙動を選択できるようにしておいてやるというのもいいでしょう。例えば、
  • コンパイル時にエラーを返す前の処理(ログを出力、abort、HOOK関数を呼ぶ等)を選択できるようにする
  • 環境変数やAPIでエラーを返す前の処理を選択できるようにする
といったことが考えられます
エラー通知の重要な役割の一つは、上位モジュールの間違いを指摘することです。そして、その目的を達成するためには、単なるエラー通知だけでなく、ログやabort,フック関数の呼び出し等とうまく組み合わせることによって、その効果は何倍も高まります。

例外スローの場合

以上では、関数リターンとしてエラー値を返すというパターンで説明しましたが、C++やJavaの例外スローも同じようなことが言えます。例外の場合、
try {
  doAction();
} catch (...) {
  /* 無視 */
} 
として、とりあえずterminateで落ちるのだけを防ぐといったコードを時々見かけます。しかし、これはエラーを無視して突き進み、事態をどんどん悪化させることにつながりかねません。例外もエラー同様に、どこでどのように処理すべきかということを明確にした上で、設計しないと行き場のない例外が氾濫することになります。

せっかくのエラー通知。しっかりと耳を傾けてあげましょう。

4873115930 【関連書籍】
デバッグルール - 9つの原則、54のヒント David J.Agans
組込み開発者におくるMISRA‐C―組込みプログラミングの高信頼化ガイド MISRA‐C研究会
C++プログラミングの処方箋―ひと味違うコードを書くための99の鉄則 Stepehn C. Dewhurst
Cプログラミングの落とし穴 A.コーニグ
組込みソフトウェア開発における品質向上の勧め―コーディング編 情報処理推進機構 SEC
Effective C++ 【改訂第2版】 Scott Meyers
組込みプレス Vol.3

コメント

非公開コメント

本のおすすめ

4274065979

4844337858

482228493X

4904807057

4873114799


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索