c++castingc++17narrowing

Is the result static_cast undefined behavior if the result does not fit into the destination type


We have an existing function in our codebase that is the following:

template <class OLD_TYPE,class NEW_TYPE>
bool ConvertFromToIsValid(const OLD_TYPE fromValIC, NEW_TYPE & toValOR) noexcept
{
    // Convert to new type
    toValOR = static_cast<NEW_TYPE> (fromValIC);

    // Convert back to OLD type and return true if there was no loss of
    // information.
    return( static_cast<OLD_TYPE> (toValOR) == fromValIC );

} // end ConvertFromToIsValid()

With the premise that if the value can be converted to the destination type, it will return true. This code wasn't actually used anywhere in our codebase. But I just tried to use it, and it doesn't work as expected.

I put it into godbolt and found that at -O0, it works as expected. At -O1, it doesn't seem to run correctly at all. At -O2 and higher, it completely optimizes out calls to this function.

I just tried pouring through the language specification. And I cannot find anywhere that specifies that this would be undefined behavior.

Specifically, I would expect that UINT64_MAX (passed as uint64_t) cannot be converted to double without a loss of precision. But it should work with a long double (at least with clang for x64).

I would assume that a behavior change between -O0 and -O2 indicates that it is undefined behavior. But like I said, I cannot find in the language specification where it says that it is.


Solution

  • Yes, behavior is undefined if in a conversion from floating-point type to integral type the value of the floating-point lies outside the range of the integral type.

    That's the case here if I assume round-to-nearest, because double can represent 2**64 exactly and it is the nearest representable value to 2**64-1, i.e. std::numeric_limits<uint64_t>::max() in your test case.

    That has nothing to do with static_cast though. This applies to implicit conversions in exactly the same way.

    Whether UB can happen with your function depends on the specific types used. There are pairs of types where undefined behavior is impossible (e.g. all integral type pairs, although there may still be implementation-defined behavior pre-C++20) and others where it is possible.

    Generally the function is safe to use only with integral types. It is not generally safe for enumeration types, floating-point types and pointer types. Behavior with class types will be completely dependent on the class behavior.

    Also, some compilers are not strictly standard-conforming with regards to floating point optimization by default. Technically static_cast and the assignment should prevent floating-point contraction and excess precision in intermediate results (which the standard permits otherwise in expressions in general), but at least GCC by default does such optimizations across cast, assignment and statements anyway and there are known bugs regarding stability of integral values resulting from such calculations. This can also break such a test.