C++で下記の2つのメソッドに3.6fを引数に渡すと異なる結果になります。
const float UNIT = 0.1f;
int test1(float num)
{
return (int) (num / UNIT);
}
int test2(float num)
{
float result = num / UNIT;
return (int) (result);
}
test1は35を返し、test2は36を返します。
3.6 / 0.1なので、36が欲しい結果なので35を返すtest1の実装だとNGなわけです。
二つのメソッドの違いは、一度計算結果をfloatの変数に入れているかどうか。
なぜこのような計算結果の違いがおこるのか、自分だけでは理解できずにteratailで質問してみました。
質問してすぐに3人の方から回答頂き、理解することができました。
結論からお話すると、「浮動小数点数の誤差」と「キャスト時の丸め処理の違い」が原因でした。
では詳しく解説していきます。
原因その1:浮動小数点数の誤差が発生していた
浮動小数点数の誤差が発生している事がわかりました。
計算結果をdoubleで受けてみると、下記の値になっていました。
35.99999849999995
計算に使っている少数が2進数だと表現できない循環小数だったのでこのような誤差が出ているんですね。
ただこれだけだと、test1とtest2の計算結果が異なる理由にはなりませんね。
test1とtest2で同じように浮動小数点数の誤差が発生しているわけですから。
原因その2:floatに代入する事で四捨五入されていた
teratailではこのように回答を頂きました。
test1はdoubleを直接intへ変換してます。test2はdoubleを一旦floatへ変換後intへ変換してます。
これは、実際にキャスト処理イメージしてみるとわかりやすいです。
test1の場合
doubleの35.99999849999995を直接intへ変換します。
doubleからintへの変換では、「小数点以下は切り捨て」になります。
なので、限りなく36に近い35.9999…でも35になってしまうのですね。
test2の場合
こちらのケースは、まずdoubleの35.99999849999995をfloatへ変換します。
この時に、floatの方がdoubleより有効桁数が小さいので、floatの有効桁数に収まるように「四捨五入」されます。
floatの有効桁数は6桁です。
有効桁数に収まるように四捨五入すると、36.0になるわけですね。
36.0のfloatをintに変換する時はそのまま36のままです。
小数点以下を切り捨てても変わらないので。
ようするに…
test1とtest2の結果の違いは、キャストの方法が違いから丸めの処理が異なっているのが原因だったというわけです、
- test1(double→int)は、小数点以下を切り捨て
- test2(double→float)は、有効桁数6桁で四捨五入
対応策
理屈がわかったところで、「test1もtest2も正確な計算結果を返す保証がない」ことがわかりました。
test1は、既に前述の通り誤った値を返しています。
test2は前述のケースでは正しい値を返していますが、たまたま四捨五入の結果正しい値になっただけです。
test1と同様に誤った値を返すケースもあるでしょう。
対応策を調べてみると、下記のような方法があるようです。
- BigDecimalを使う
- 整数で処理する
- 有効桁を決めて四捨五入する
おわりに
計算系の処理を実装する時は気をつけないといけませんね。
特に小数点を扱うような処理の時は要注意です。
それにしても、teratailにはすごく助けられました。
投稿してから最初の回答をもらうまで5分と、早すぎてびっくりしました。
一人で考えていると煮詰まっている時は、誰かの意見を聞くとパッと視界がクリアになって理解できることも多いです。
自宅で一人で仕事していて、相談相手がいない僕のような人でも気軽に質問できるteratailは救世主的存在ですね。
プログラミングで困ったことがあれば、是非使って見る事をオススメします。
この記事が少しでも参考になれば幸いです。
最後までお読み頂きありがとうございました。