Hacker News
C++ float-to-int conversion can be undefined behavior
digitalPhonix
|next
[-]
> 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)
Isn't the outcome of the UB (ie. whether it will "rm -rf /" or something else) dependent on both the target and the compiler? And the compiler (or future compiler) could plausibly make the assumption that the narrowing to an unrepresentable value will never occur and change behaviour because of it?
wavemode
|root
|parent
|next
[-]
https://github.com/microsoft/GSL/issues/786#issuecomment-513...
> I'll raise this issue in the next internal GSL sync. I'd agree with y'all that this behavior: https://godbolt.org/z/4Tr1fe9xG is undesirable
mort96
|root
|parent
[-]
The problem isn't, "oh no what if my CPU's float->int conversion instruction traps", that's an extremely naive way to think about UB. Everyone who has thought seriously about UB in C++ for any length of time knows this. It's worrying that this was Sutter's response.
20k
|root
|parent
|next
|previous
[-]
There's also important context in that Herb is currently one of the people leading the current memory safety approach for C++
jcranmer
|root
|parent
|next
|previous
[-]
That said, I'm a little hard-pressed to think of optimizations that would actually take advantage of poison, because floating-point range isn't really computed in the optimizer.
mort96
|root
|parent
[-]
I don't know exactly which optimization passes do what, but a few observations:
* The 'foo(unsigned int n)' function should never return a value that's greater than 'n', since it returns 'i < n ? i : n'.
* The value printed by the 'foo' function should always be the same as the value that's returned.
Yet the value it prints is 2700624104 (which is greater than 'n', which is 10 in this case), and the returned value is 2700623376, which is different. (The exact numbers vary run to run)
If the conversion "just" resulted in a bogus value, we would have expected some number <=10 to be printed two times.
Maxatar
|root
|parent
|next
|previous
[-]
GCC 12, 13, 14
XCode 14.3.1, 15.4
Clang 16, 17, 18
Visual Studio with MSVC VS2019, VS2022
Visual Studio with LLVM VS2019, VS2022
pjmlp
|root
|parent
|next
|previous
[-]
Meaning MSVC is aware of these cases, so the compiler has special cases for it.
digitalPhonix
|root
|parent
|next
[-]
pjmlp
|root
|parent
[-]
aw1621107
|root
|parent
[-]
[0]: https://github.com/microsoft/GSL/tree/99a29ce797c8337b8923f2...
wavemode
|root
|parent
|next
|previous
[-]
> The GSL officially supports recent major versions of Visual Studio with both MSVC and LLVM, GCC, Clang, and XCode with Apple-Clang
pjmlp
|root
|parent
[-]
Maxatar
|root
|parent
[-]
achierius
|root
|parent
[-]
gpvos
|next
|previous
[-]
20k
|root
|parent
[-]
pjmlp
|next
|previous
[-]
dmitrygr
|next
|previous
[-]
This will do wonders for speed. Actually explicitly using the safe isntr might be better. Something like this will happily compile to a single instr and cause you no grief even if the compiler had it out for you with UB. These instrs all clearly define outputs for all inputs (note that said outputs may not match across architectures)
static inline __attribute__((always_inline)) int f2i(float myFloat) {
int myInt;
#if defined(__arm__)
asm("VCVT.S32.F32 %0, %1":"=r"(myInt), "t"(myFloat));
#elif defined (__aarch64__)
asm("FCVTZS %0, %1":"=r"(myInt), "w"(myFloat));
#elif defined (__x86_64__)
asm("CVTTSS2SI %0, %1":"=r"(myInt), "x"(myFloat));
#else
#if 0 // be boring
if (myFloat <= TOO_SMALL_FLOAT || myFloat => TOO_BIG_FLOAT)
abort();
#else
#warning "Embrace the UB"
#endif
myInt = (int)myFloat;
#endif
return myInt;
}
orangepanda
|next
|previous
[-]
cataphract
|root
|parent
|previous
[-]
Maxatar
|root
|parent
[-]
This is simply false and an oft repeated myth. Undefined behavior has a specific technical definition that is in the C++ standard [1] and there is absolutely no mention in that definition or the implication of that definition that undefined behavior necessarily results in an invalid or incorrect program.
The definition of undefined behavior, right from the standard itself is... and I quote... get ready for it...
"behavior for which this document imposes no requirements"
That's it, nothing more, nothing less.
The standard even goes out of its way to state the following:
"Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, *to behaving during translation or program execution in a documented manner* characteristic of the environment".
Behaving in a documented manner characteristic of an environment is a far cry from being by incorrect by definition.
[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n49...
20k
|root
|parent
|next
[-]
That's saying that programs that exhibit undefined behaviour are not governed by the C++ spec. For a program to be a valid, spec governed piece of C++ code it has to exhibit no undefined behaviour (outside of some constraints). Its accurate to say that any undefined behaviour results in the code being executed no longer being C++, and it can have any behaviour. That's synonymous in common developer speak with 'incorrect', as its desirable for your C++ code to be executed as C++
Joker_vD
|root
|parent
|next
|previous
[-]
Honestly, you'd have a better argument by quoting that "Correct execution" can include undefined behavior and erroneous behavior, depending on the data being processed". Which is quite a wild sentence to read, but here we are.