Your question is exactly this:
Say uint8_t a = (uint8_t)123456;
is defined given the wrapping around, uint8_t a = (uint8_t)123456.7
is UB as C standard does not require the wrapping. Is this what the standard says?
The language of the Standard seems unambiguous about that and the footnote confirms that the modulo operation that is defined for integer conversions does not necessarily occur for floating point conversions.
This text was already present in the C99 version of the C Standard (with a different footnote number), and also in the C90 version (aka ANSI C) without a reference to the _Bool
type.
The reason for this apparent semantic inconsistency in the C Standard is probably the concern to keep existing implementations and hardware behavior compatible with the Standard. It may be linked to the binary representation of negative floating point numbers: while all but some ancient architectures have used two's complement representation for signed integers for a long time (this is actually mandated by the latest C23 Standard), floating point numbers generally use sign + magnitude representations. The modulo semantics of signed integer to unsigned integer conversions costs nothing on two's complement representations, but would require extra silicon for floating point values, which was not present on all current hardware implementations at the time. The Standard Committee decided to keep these cases undefined for uint32_t = (uint32_t)-1.23;
and also for the less problematic uint8_t a = (uint8_t)123456.7;
to avoid the requirement for compiler writers to produce extra costly code to fix the behavior on hardware that does not implement the modulo semantics already.
Note that the C23 has a slightly different spcification for the conversion from floating point to integer types:
6.3.1.4 Real floating and integer
1 When a finite value of standard floating type is converted to an integer type other than bool
, the fractional part is discarded (i.e., the value is truncated toward zero). If the value of the integral part cannot be represented by the integer type, the behavior is undefined.66)
2 When a finite value of decimal floating type is converted to an integer type other than bool
, the fractional part is discarded (i.e., the value is truncated toward zero). If the value of the integral part cannot be represented by the integer type, the "invalid" floating-point exception shall be raised and the result of the conversion is unspecified.
Footnote: 66) The remaindering operation performed when a value of integer type is converted to unsigned type need not be performed when a value of real floating type is converted to unsigned type. Thus, the range of portable real floating values is (−1
, U
type_MAX + 1
).
The behavior is more explicit for conversions from decimal floating point representations to integer: a floating point exception must be raised if the value is not representable in the target type, which seems a very strong constraint as there are at least 8 and possibly more integral types to handle specifically, not counting the bit-precise integer types...