浮動小数点数から整数への型変換

高木です。おはようございます。

技術情報も書くと宣言したので、とりあえず何か書いておくことにします。

今回の話題は浮動小数点数から整数への型変換についてです。
先日仕事でうっかりやってしまったミスの反省を踏まえて、自分自身の備忘録もかねて書き留めておきます。

私が仕事で使っているプログラミング言語は、現在はC++とJavaが中心で、他にはC、C#、PHPなどがあります。
多くはCと類似した文法を持つ言語ですが、中にはTclのように全然違う言語仕様のものもあります。

今回やってしまったミスは、C++における浮動小数点数から汎整数型への型変換に関するものでした。

C++では、符号付き整数型から符号無し整数型に型変換するとき、変換先の型の最大値+1を法とする剰余になります。
符号無し整数型は決してオーバーフローしないのです。
その感覚で、浮動小数点型から符号無し整数型に安易に型変換してしまっていたのです。

インテルのプロセッサでは、確かにそのように振る舞うようです。
ところが、ARMでは全然違う動作になってしまいます。

最初、コンパイラやハードウェアのバグも含めて疑ったのですが、標準規格を調べてみて自分のミスに気づきました。
C++の規格書であるJIS X3014:2003から該当部分を引用します。

4.9 浮動小数点数と汎整数の間の変換 浮動小数点型の右辺値は、整数型の右辺値に変換することができる。その変換は、切り捨て変換とする。つまり、小数部分を切って捨てる。切り捨てた値が、目的の型で表現できない場合、その挙動は未定義とする。

これで合点がいきました。
符号無し整数型の表現範囲を超えた浮動小数型の値を変換しようとすると未定義の動作になるのです。
だから、インテルとARMで違う結果になったのです。

この機会に他の言語についても調べてみました。
まずはCからです。
JIS X3010:2003から該当部分を引用します。

6.3.1.4 実浮動小数点型及び整数型 実浮動小数点型の有限の値を_Bool型以外の整数型に型変換する場合、小数部を捨てる(すなわち、値を0方向に切り捨てる。)。整数部の値が整数型で表現できない場合、その動作は未定義とする。

表現は異なりますが、いっていることはC++と同じです。
ちなみに、C++でもbool型は例外になっています。

次に、Javaについても調べてみましょう。
若干古い資料ですが、http://www.y-adagio.com/public/standards/tr_javalang/5.doc.htm#175672 を参照しました。

浮動小数点数から整数型 T への縮小変換は,二つの段階を踏む。

  1. 第一段階では,浮動小数点数を,T が long ならば long に変換し,T が byte,short,char 又は int ならば int に変換する。
    • 浮動小数点数がNaN (4.2.3)ならば,変換の第一段階の結果は,int 又は long の 0 とする。
    • そうでないときには,浮動小数点数が無限大でなければ,浮動小数点数は,IEEE 754のゼロへ向かう丸めモード(4.2.3)を使ってゼロの方向の整数値 V に丸める。このとき次の二つの場合が存在する。
      • T が long であって,この整数値が long で表現できれば,第一段階の結果は, long の値 V とする。
      • そうでないときには,この整数値が int として表現できれば,第一段階の結果は,int の値 V とする。
    • そうでないときには,次の二つのいずれかが真でなければならない。
      • 値が小さすぎる(膨大な負の値又は負の無限大)。第一段階の結果は,型 int 又は型 long の最小表現可能値とする。
      • 値が大きすぎる(膨大な正の値又は正の無限大)。第一段階の結果は,型 int 又は型 long の最大表現可能値とする。
  2. 第二段階は,次のとおりとする。
    • T が int 又は long ならば,変換の結果は,第一段階の結果とする。
    • T が byte,char 又は short ならば,変換の結果は,第一段階の結果の型 T への縮小変換(5.1.3)の結果とする。

非常に厳密に規定されているので、今回の私がやってしまったミスが入り込む余地はなさそうです。
もっとも、この仕様を実現するためには決して小さくないオーバーヘッドが発生するはずですので、よいことばかりではないでしょう。

次にC#についても調べてみました。
やはりちょっと古いですが、C# Language Specification 5.0からの引用です。

For a conversion from float or double to an integral type, the processing depends on the overflow checking context (§7.6.12) in which the conversion takes place:

  • In a checked context, the conversion proceeds as follows:
    • If the value of the operand is NaN or infinite, a System.OverflowException is thrown.
    • Otherwise, the source operand is rounded towards zero to the nearest integral value. If this integral value is within the range of the destination type then this value is the result of the conversion.
    • Otherwise, a System.OverflowException is thrown.
  • In an unchecked context, the conversion always succeeds, and proceeds as follows.
    • If the value of the operand is NaN or infinite, the result of the conversion is an unspecified value of the destination type.
    • Otherwise, the source operand is rounded towards zero to the nearest integral value. If this integral value is within the range of the destination type then this value is the result of the conversion.
    • Otherwise, the result of the conversion is an unspecified value of the destination type.

C#の場合、checkedコンテキストかどうかで大きく振るまいが変わるようです。
checkedコンテキストではオーバーフロー例外が発生しますが、それ以外はCやC++に近いようです。
ただし、CやC++では未定義の動作でしたが、C#では未規定の値になるとのことなので多少はましになっています。

最後にPHPについても調べてみました。
以下は http://jp2.php.net/manual/ja/language.types.integer.php#language.types.integer.casting からの引用です。

浮動小数点数から

float から整数に変換する場合、その数はゼロのほうに丸められます。

float が整数の範囲 (通常は、32 ビットプラットフォームでは +/- 2.15e+9 = 2^31、Windows 以外の 64 ビットプラットフォームでは +/- 9.22e+18 = 2^63 ) を越える場合、結果は undefined となります。これは、 その float が正しい整数の結果を得るために十分な精度を得られなかったからです。 この場合、警告も通知も発生しません!

このあたりはCやC++と変わらないようです。

さらに次のような注意書きがあります。

注意:
NaN や無限大を integer にキャストした結果は 未定義でプラットフォーム依存でしたが、PHP 7.0.0 以降は常にゼロとなります。

バージョン7.0.0以降を前提としないなら、結局のところNaNや無限大からのキャストは未定義の動作と考えた方がよさそうです。

以上、いくつかのプログラミング言語について浮動小数点数から整数への型変換の仕様を見てきました。
言語によってかなり仕様がことなることがわかります。

CやC++は、処理系による振る舞いの違いが多いことが、扱いを難しくしている要因のひとつになっています。
ところが、世間的には簡単とされているPHPであっても、プラットフォームやバージョンによって振る舞いが違っています。

こうした細かい点を気にするプログラマーがどの程度いるのかわかりませんが、少なくとも私は気になります。
だから、今回のミスは本当に不覚でした。