こんにちは高木です。
昔からよく言われているノウハウとして、「浮動小数点数には等価演算子を使ってはならない」があります。プログラミング言語によって用語や演算子が少しずつ異なるので、最初にここでいう「等価演算子」について認識合わせをしておくことにします。「等価演算子」というのは、C言語では==と!=の総称であり、ここでもその意味で使っています。C言語のシンタックスを借用している言語であれば同様の演算子があるはずです。また、===や!==演算子がある言語では、==や!=より===や!==を使うべきと言われることが多いのですが、ここではその話はしません。
なぜ、浮動小数点数で等価演算子を使うべきではないと言われるかについて、簡単におさらいしておくことにしましょう。わかりやすい例としてはループカウンタがありますね。
0 1 2 3 4 5 6 |
double x; for (x = 0; x != 1; x += 0.1) { ... } |
多くの処理系では浮動小数点数の内部表現に2進数を使っています。2進数の浮動小数点数では0.1という値を正確に表現することができませんので、0.1を10回足しても1にはなりません。結果として、このfor文はいつまで経っても終了せずに回り続けてしまいます。
そういう事情ですので、今回の場合に限れば、内部表現が10進数であればループが止まらなくなることはありません。
0 1 2 3 4 5 6 |
_Decimal64 x; for (x = 0; x != 1; x += 0.1dl) { ... } |
一部の処理系でしか使えませんが、上記のように10新浮動小数点数を使えば期待通り10回繰り返してループは終了します。ですが、浮動小数点数の精度が有限である以上、どうしても表現しきれば値は出てきます(たとえば\(\frac{1}{3}\))ので、ループカウンタのような用途では、やはり等価演算子を使って継続条件を書くのは避けるべきです。
では、次のような場合はどうでしょうか?
二次方程式の解を求めるquadratic関数を作ってみることにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int quadratic(double a, double b, double c, double ans[static 2]) { if (a == 0) // b*x + c = 0の解を求める。 { ans[0] = -c / b; return 1; } double D = b*b - 4*a*c; // 判別式 if (D < 0) { return 0; } else if (D == 0) { ans[0] = -b / (2*a); return 1; } double sqrt_D = sqrt(D); ans[0] = (-b + sqrt_D) / (2*a); ans[1] = (-b - sqrt_D) / (2*a); return 2; } |
ちょっと複雑ですが、中学校で習った二次方程式の解の公式をそのまま実装しました。二次方程式 \(ax^2 + bx + c = 0\) の解の公式は次の通りでしたね。
$$x = \frac{-b \pm \sqrt{b^2 – 4ac}}{2a}$$
今回は虚数を扱うのはやめようと思いますので、判別式 \(D = b^2 – 4ac\) の符号を調べれば解の個数がわかります。すなわち、\(D\)が正であれば解は2個、0であれば重解なので解は1個、負であれば解無しなので0個です。
さて、上のコードをよく見てみると、浮動小数点の比較に等価演算子を使っているところが2箇所あります。
1つ目は3行目で、aが0かどうかを判定しています。解の公式を見ればわかるように、aが0であればゼロ除算が発生してしまいますので、\(bx + c = 0\) という一次関数の解を求めるようにしないといけません。2つ目は14行目で、判別式\(D\)が0かどうかを判定しています。判別式の符号によって解の個数が変わるので当然のことです。
ここで少し考えていただきたいのですが、浮動小数点数の比較に等価演算子を絶対使ってはいけないのであれば、これら2箇所は一体どのように書けばいいのでしょうか? 1つ目の判定はともかく、2つ目の判定では判別式は計算結果ですので誤差を含んでいる可能性はもちろんあります。
2つ目については意見が分かれるかもしれませんが、私はどちらも素直に等価演算子を使って0と比較すればいいと考えています。それ以外に有効な判定方法はありませんから。
このように、浮動小数点数の比較に等価演算子を使うかどうかはケースバイケースではないでしょうか? ゼロ除算を回避する場合など、むしろ等価演算子を使うべき状況もあるのです。