Compilando el siguiente código:
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; }Salidas (MSVC x86):
INT_MAX: 2147483647 UINT_MAX: 4294967295 Double value: 2147483649.000000 Direct cast value: 2147483648 Indirect cast value: 2147483649Salidas (MSVC x64):
INT_MAX: 2147483647 UINT_MAX: 4294967295 Double value: 2147483649.000000 Direct cast value: 2147483649 Indirect cast value: 2147483649 En la documentación de Microsoft no se menciona el valor máximo de entero con signo en las conversiones de double a unsigned int .
Todos los valores por encima de INT_MAX se truncan a 2147483648 cuando es el retorno de una función.
Estoy usando Visual Studio 2019 para construir el programa. Esto no sucede en gcc .
¿Estoy haciendo algo mal? ¿Hay alguna forma segura de convertir double a unsigned int ?
Un error del compilador...
Desde el ensamblado proporcionado por @anastaciu, el código de transmisión directa llama a __ftol2_sse , que parece convertir el número en un largo con signo. El nombre de la rutina es ftol2_sse porque esta es una máquina habilitada para sse, pero el flotante está en un registro de punto flotante x87.
; 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, 8El reparto indirecto, por otro lado, no
; 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 que extrae y almacena el valor doble en la variable local, luego lo carga en un registro SSE y llama a __dtoui3 , que es una rutina de conversión de doble a int sin firmar...
El comportamiento del elenco directo no se ajusta a C89; ni se ajusta a ninguna revisión posterior, incluso C89 dice explícitamente que:
La operación de resto que se realiza cuando un valor de tipo integral se convierte en un tipo sin signo no necesita realizarse cuando un valor de tipo flotante se convierte en un tipo sin signo. Por lo tanto, el rango de valores portátiles es [0, Utype_MAX + 1) .
Creo que el problema podría ser una continuación de esto desde 2005 : solía haber una función de conversión llamada __ftol2 que probablemente habría funcionado para este código, es decir, habría convertido el valor en un número con signo -2147483647, que habría producido el resultado correcto cuando se interpreta como un número sin signo.
Desafortunadamente __ftol2_sse no es un reemplazo directo para __ftol2 , ya que, en lugar de simplemente tomar los bits de valor menos significativos tal como están, señalaría el error fuera de rango devolviendo LONG_MIN / 0x80000000 , que, interpretado como unsigned long aquí no es en absoluto lo que se esperaba. El comportamiento de __ftol2_sse sería válido para signed long con signo, ya que la conversión de un valor doble a > LONG_MAX a signed long con signo tendría un comportamiento indefinido.
Nadie ha mirado el asm para __ftol2_sse de MS.
A partir del resultado, podemos inferir que probablemente se convirtió de x87 a int / long firmado (ambos tipos de 32 bits en Windows), en lugar de convertirse de forma segura a uint32_t .
x86 FP -> instrucciones de enteros que desbordan el resultado entero no solo envuelven/truncan: producen lo que Intel llama "entero indefinido" cuando el valor exacto no se puede representar en el destino: conjunto de bits alto, otros bits claros. es decir 0x80000000 .
(O si la excepción no válida de FP no está enmascarada, se dispara y no se almacena ningún valor. Pero en el entorno de FP predeterminado, todas las excepciones de FP están enmascaradas. Es por eso que para los cálculos de FP puede obtener un NaN en lugar de una falla).
Eso incluye instrucciones x87 como fistp (usando el modo de redondeo actual) e instrucciones SSE2 como cvttsd2si eax, xmm0 (usando truncamiento hacia 0, eso es lo que significa la t adicional).
Por lo tanto, es un error compilar double -> conversión unsigned en una llamada a __ftol2_sse .
Nota al margen / tangente:
En x86-64, FP -> uint32_t se puede compilar en cvttsd2si rax, xmm0 , convirtiéndolo en un destino firmado de 64 bits, produciendo el uint32_t que desea en la mitad inferior (EAX) del destino entero.
Es C y C ++ UB si el resultado está fuera del rango 0..2 ^ 32-1, por lo que está bien que los valores positivos o negativos enormes dejen la mitad inferior de RAX (EAX) cero del patrón de bits indefinido entero. (A diferencia de las conversiones de entero-> entero, la reducción del módulo del valor no está garantizada. ¿Está definido en el estándar C el comportamiento de lanzar un doble negativo a int sin signo? Comportamiento diferente en ARM vs. x86 . Para ser claros, nada en la pregunta es un comportamiento indefinido o incluso definido por la implementación. Solo estoy señalando que si tiene FP->int64_t, puede usarlo para implementar de manera eficiente FP->uint32_t. Eso incluye x87 fistp que puede escribir un destino entero de 64 bits incluso en modo de 32 y 16 bits, a diferencia de las instrucciones SSE2 que solo pueden manejar directamente números enteros de 64 bits en modo de 64 bits.