Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Double cast to unsigned int on Win32 is truncating to 2,147,483,648

Compiling the following code:

double getDouble() {     double value = 2147483649.0;     return value; }  int main() {      printf("INT_MAX: %u\n", INT_MAX);      printf("UINT_MAX: %u\n", UINT_MAX);       printf("Double value: %f\n", getDouble());      printf("Direct cast value: %u\n", (unsigned int) getDouble());      double d = getDouble();      printf("Indirect cast value: %u\n", (unsigned int) d);       return 0; } 

Outputs (MSVC x86):

INT_MAX: 2147483647 UINT_MAX: 4294967295 Double value: 2147483649.000000 Direct cast value: 2147483648 Indirect cast value: 2147483649 

Outputs (MSVC x64):

INT_MAX: 2147483647 UINT_MAX: 4294967295 Double value: 2147483649.000000 Direct cast value: 2147483649 Indirect cast value: 2147483649 

In Microsoft documentation there is no mention to signed integer max value in conversions from double to unsigned int.

All values above INT_MAX are being truncated to 2147483648 when it is the return of a function.

I'm using Visual Studio 2019 to build the program. This doesn't happen on gcc.

Am I doing someting wrong? Is there a safe way to convert double to unsigned int?

like image 762
Matheus Rossi Saciotto Avatar asked Sep 20 '20 19:09

Matheus Rossi Saciotto


People also ask

Can you cast int to unsigned?

You can convert an int to an unsigned int . The conversion is valid and well-defined. Since the value is negative, UINT_MAX + 1 is added to it so that the value is a valid unsigned quantity. (Technically, 2N is added to it, where N is the number of bits used to represent the unsigned type.)

What happens when you cast signed to unsigned in C?

A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.


1 Answers

A compiler bug...

From assembly provided by @anastaciu, the direct cast code calls __ftol2_sse, which seems to convert the number to a signed long. The routine name is ftol2_sse because this is an sse-enabled machine - but the float is in a x87 floating point register.

; Line 17     call    _getDouble     call    __ftol2_sse     push    eax     push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@     call    _printf     add esp, 8 

The indirect cast on the other hand does

; Line 18     call    _getDouble     fstp    QWORD PTR _d$[ebp] ; Line 19     movsd   xmm0, QWORD PTR _d$[ebp]     call    __dtoui3     push    eax     push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@     call    _printf     add esp, 8 

which pops and stores the double value to the local variable, then loads it into a SSE register and calls __dtoui3 which is a double to unsigned int conversion routine...

The behaviour of the direct cast does not conform to C89; nor does it conform to any later revision - even C89 explicitly says that:

The remaindering operation done when a value of integral type is converted to unsigned type need not be done when a value of floating type is converted to unsigned type. Thus the range of portable values is [0, Utype_MAX + 1).


I believe the problem might be a continuation of this from 2005 - there used to be a conversion function called __ftol2 which probably would have worked for this code, i.e. it would have converted the value to a signed number -2147483647, which would have produced the correct result when interpreted an unsigned number.

Unfortunately __ftol2_sse is not a drop-in replacement for __ftol2, as it would - instead of just taking the least-significant value bits as-is - signal the out-of-range error by returning LONG_MIN / 0x80000000, which, interpreted as unsigned long here is not at all what was expected. The behaviour of __ftol2_sse would be valid for signed long, as conversion of a double a value > LONG_MAX to signed long would have undefined behaviour.

like image 107