プログラミング
C++のfloatからintへの変換は未定義の動作となる可能性がある
C++ float-to-int conversion can be undefined behavior (kttnr.net)
要約
C++では、浮動小数点数(float)を整数型(int)に変換する際に、変換先の整数型に値が収まらない場合、未定義の動作(Undefined Behavior, UB)となります。この変換はコード上で簡単に行えてしまうため、多くのコードで意図せずこの問題が発生しています。コンパイラやハードウェアによっては問題なく動作するように見えても、異なる環境では予期せぬ結果を招く可能性があります。
全文翻訳
C++のfloatからintへの変換は未定義の動作となる可能性がある
C++でfloatをintに変換する際、切り捨てられたfloatが宛先整数型に収まらない場合、未定義の動作となります。C++では、これを誤って行うことが容易です。多くのコードがこれを間違っています。
void foo(float f) {
int i0 = f;
int i1 = int(f);
int i2 = static_cast<int>(f);
}
このコードは、-Wallや-Wextraを使用しても、警告を一切生成しません。-Wconversionは暗黙の変換についてのみ警告します。しかし、3つの変換のそれぞれが、一部の入力に対して未定義の動作となります。
Cppreferenceの暗黙の変換ページ、「浮動小数点-整数変換」のセクションには次のように記載されています。
浮動小数点型のprvalueは、任意の整数型へのprvalueに変換できます。小数部分は切り捨てられます。つまり、小数部分は破棄されます。値が宛先型に収まらない場合、未定義の動作となります(宛先型がunsignedであっても、モジュロ演算は適用されません)。
私はこの間違いを現場で何度も見てきました。例えば、C++ Core GuidelinesをサポートするMicrosoftのGuidelines Support Libraryなどです。
GSLは、安全なナローイング変換のための関数、gsl::narrowを提供しています。
gsl::narrow<T>(x)は、ナローイング変換のための名前付きキャストであり、符号なしプロモーションなしでstatic_cast<T>(x)を実行します。引数xがターゲット型Tで表現できない場合、関数は例外をスローします。
floatからintへの変換をどのように処理しているのか興味がありました。ドキュメントとは異なり、実際にはそれらを処理していないことが判明しました。gsl::narrowも一部の入力に対して未定義の動作となります。私はコメントでこれを指摘しましたが、以下の理由で却下されました。
内部でのUBの使用について:それは問題ありません。もし誰かがそれを心配するなら、使用されているUBはターゲットとするプラットフォームでは無害です(例えば、これらの型に対するハードウェアトラップ表現にヒットすることはありません)。
未定義の動作は現実のものですが、現在のプロセッサとコンパイラでは、プログラムは通常それでも動作します。コンパイラはx86のCVTTSS2SIのような命令を選択する傾向があり、これは表現不可能なすべての入力を同じプレースホルダー整数INT_MINにマッピングします。AArch64にはFCVTZSがあり、これは飽和させ、NaNをゼロにマッピングします。
プログラムがクラッシュしないと、問題は無害に見えることがあります。しかし、そうではありません。異なるハードウェアで異なる結果が得られることは問題です。さらに重要なのは、実行される未定義の動作は問題があるということです。Ralf Jungは、彼の投稿「What The Hardware Does」is not What Your Program Doesでこれをうまく説明しています(彼のブログをお勧めします)。コンパイラが異なる変換を適用した場合、コードが突然機能しなくなる可能性があります。
正しい修正は、キャストする前に範囲チェックを行うことです。私はこれをRustの飽和アプローチに基づいた概念実証ライブラリにしました。
ClangとGCCの未定義動作サニタイザー、-fsanitize=float-cast-overflowを参照して、未定義の動作を検出することもできます。私はUBSanですべてのC++コードをテストすることを推奨します。
誤ったGSLの理由は、問題コメントからコードにまで及んでいます。問題は修正されていません。