エラー握りつぶし

ありがちダメなコードのダメな理由を書くコーナー。

#include <stdlib.h>
#include <math.h>
#include <stdio.h>

int mysqrt(int x) {
  if (x < 0) {
    return 0;
  } else {
    return (int)sqrt(x);
  }
}

int main() {
  printf("%d\n", mysqrt(4));
  printf("%d\n", mysqrt(1));
  printf("%d\n", mysqrt(0));
  printf("%d\n", mysqrt(-1));
  
  printf("%f\n", sqrt(4));
  printf("%f\n", sqrt(1));
  printf("%f\n", sqrt(0));
  printf("%f\n", sqrt(-1));
  
  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -lm -g tmp.c
$ ./a.out
2
1
0
0
2.000000
1.000000
0.000000
nan

と出力された。

"nan"って何?

"nan"とは"Not a Number"の略で、和訳時には非数と称されるもの*1。文字通り、数値では表せない値を表現するのに用いられる。次のような条件の場合、CPU例外として扱われるか、または計算結果としてNaNを返す。

  • 数学関数に定義域外の引数を与えた。
  • 0で割り算を行った。

何がダメなのか。

大抵のNaNは途中の計算にエラーがあったことを示す。そのNaNをintにキャストした結果はただの0なので(intにNaNはない)、mysqrt(0) == mysqrt(-1)というように、エラーが通常出力に紛れて判別不能になっている。
この類の間違いは正常処理に影響を与えない/与えにくいために放置されることが多い。しかし、例外処理/異常処理として扱われるべきものが正常処理に紛れ込んでいると、大量の処理結果を並べるとどこか歪なものになるだろう。
例えば、上の例では入力に負数を含む分だけ出力の0点にスパイクが立つことになる。これを「この関数はこういう特性だから〜」と後処理で切り捨てるといった手法はカーゴカルトプログラミング*2そのものだ。

〆。

基本は次の分岐表にしたがってください。

  • 呼び出し先はエラーを返すか?
    • エラーを返さない→値をそのまま返して良い
    • エラーを返すかもしれない→エラーチェックした結果は?
      • 正常→値をそのまま返して良い。
      • 異常→リカバリは可能か?
        • 可能→リカバリ結果は?
          • 成功→リカバリ結果を返す。
          • 失敗→自分もエラーを返す。
        • 不可能→自分もエラーを返す。

追記

こういった「失敗を検出できない仕組み」はフェイル・セーフの観点からもダメなコード。NaNを返すことができない環境でも最低限ログを出力すること。

int mysqrt(int x) {
  if (x < 0) {
    LOG_PRINTF("[%s:%d] Invalid argument! x < 0\n", __FILE__, __LINE__);
    return 0;
  } else {
    return (int)sqrt(x);
  }
}