エラー握りつぶし
ありがちダメなコードのダメな理由を書くコーナー。
#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);
}
}