C++ float-to-int conversion can be undefined behavior
C++ float-to-int conversion can be undefined behavior
C++ 中浮点数转整数可能导致未定义行为
C++ float-to-int conversion can be undefined behavior. Converting a float to an int in C++ is undefined behavior when the truncated float does not fit into the destination integer. C++ makes it easy to do this accidentally. Much code gets this wrong. 在 C++ 中,将浮点数转换为整数时,如果截断后的浮点数无法放入目标整数类型中,则会导致未定义行为(Undefined Behavior, UB)。C++ 很容易让人无意中触发这种情况,许多代码都存在这一错误。
void foo(float f) {
int i0 = f;
int i1 = int(f);
int i2 = static_cast<int>(f);
}
This code does not generate any warnings, not even with -Wall and -Wextra. -Wconversion only warns about the implicit conversion. Yet each of the three conversions is undefined behavior for some inputs.
这段代码不会产生任何警告,即使开启了 -Wall 和 -Wextra 选项也是如此。-Wconversion 仅会对隐式转换发出警告。然而,对于某些输入值,这三种转换方式都会导致未定义行为。
Cppreference’s implicit conversion page, section Floating-integral conversions, states: A prvalue of floating-point type can be converted to a prvalue of any integer type. The fractional part is truncated, that is, the fractional part is discarded. If the value cannot fit into the destination type, the behavior is undefined (even when the destination type is unsigned, modulo arithmetic does not apply). Cppreference 关于隐式转换页面的“浮点-整数转换”章节指出:浮点类型的纯右值(prvalue)可以转换为任何整数类型的纯右值。小数部分会被截断,即直接丢弃。如果该值无法放入目标类型中,则行为是未定义的(即使目标类型是无符号类型,模运算也不适用)。
I have seen this mistake many times in the wild. For example, in Microsoft’s Guidelines Support Library, which supports the C++ Core Guidelines. 我在实际开发中多次见过这种错误。例如,在支持 C++ 核心准则(C++ Core Guidelines)的微软 Guidelines Support Library (GSL) 中就存在这种情况。
GSL provides a function for safe narrowing conversions, gsl::narrow: gsl::narrow<T>(x) is a named cast that does a static_cast<T>(x) for narrowing conversions with no signedness promotions. If the argument x cannot be represented in the target type T, then the function throws.
GSL 提供了一个用于安全窄化转换的函数 gsl::narrow:gsl::narrow<T>(x) 是一个具名转换,它执行 static_cast<T>(x) 来进行窄化转换,且不进行符号扩展。如果参数 x 无法在目标类型 T 中表示,该函数会抛出异常。
I was curious how they addressed float-to-int conversion. It turns out, contrary to the docs, that they do not address it. gsl::narrow is undefined behavior for some inputs. I pointed this out in a comment and was dismissed with the following reasoning:
我很好奇他们是如何处理浮点转整数的。结果发现,与文档描述相反,他们并没有处理这个问题。gsl::narrow 对于某些输入会导致未定义行为。我在评论中指出了这一点,但被对方以如下理由驳回:
“Regarding the use of UB internally: It’s okay and if anyone is worried about it the use of UB is benign on the platforms we target (e.g., they don’t involve hitting any hardware trap representations for these types).” “关于内部使用 UB 的问题:这是可以接受的。如果有人担心,在我们所针对的平台上,这种 UB 是良性的(例如,它们不会触发这些类型的硬件陷阱表示)。”
The undefined behavior is real, but with current processors and compilers, the program usually works anyway. Compilers tend to pick an instruction like x86’s CVTTSS2SI, which maps every unrepresentable input to the same placeholder integer INT_MIN. AArch64 has FCVTZS, which saturates and maps NaN to zero.
未定义行为是真实存在的,但在当前的处理器和编译器下,程序通常仍能运行。编译器倾向于选择类似 x86 的 CVTTSS2SI 指令,它将所有无法表示的输入映射为同一个占位整数 INT_MIN。AArch64 则有 FCVTZS 指令,它会进行饱和处理并将 NaN 映射为零。
Your program not crashing can make the problem seem benign. It is not. Different results on different hardware are problematic. More importantly, any undefined behavior that executes is problematic. Ralf Jung explains this well in his post “What The Hardware Does” is not What Your Program Does. (I recommend his blog.) Your code could suddenly stop working when the compiler happens to apply a different transformation. 程序没有崩溃可能会让你觉得这个问题是良性的。事实并非如此。在不同硬件上产生不同的结果本身就是个问题。更重要的是,任何被执行的未定义行为都是有问题的。Ralf Jung 在他的文章《“硬件的行为”并非“程序的行为”》中对此做了很好的解释(我推荐阅读他的博客)。当编译器恰好应用了不同的优化转换时,你的代码可能会突然停止工作。
The correct fix is to bounds check before casting. I turned this into a proof of concept library based on Rust’s saturating approach. 正确的修复方法是在转换前进行边界检查。我基于 Rust 的饱和处理方法,将其做成了一个概念验证库。
You can also detect the undefined behavior with Clang’s and GCC’s Undefined Behavior Sanitizer, see -fsanitize=float-cast-overflow. I recommend testing all C++ code with UBSan anyway.
你也可以使用 Clang 和 GCC 的未定义行为检测工具(UBSan)来检测此类问题,请参考 -fsanitize=float-cast-overflow。无论如何,我都建议使用 UBSan 测试所有的 C++ 代码。
The faulty GSL reasoning has made it from the issue comment into the code. The problem has not been fixed. GSL 中这种错误的逻辑已经从 Issue 评论进入了代码库。目前该问题尚未得到修复。