Por qué los enteros sorprenden
Un entero de máquina tiene un número fijo de bits, así que cualquier operación puede producir un valor que no cabe. Lo que pasa entonces no lo decide el hardware (que simplemente da la vuelta), sino el lenguaje:
| Lenguaje | Desbordamiento con signo | Desbordamiento sin signo | Desplazamiento ≥ anchura | División entre cero |
|---|---|---|---|---|
| C / C++ | comportamiento indefinido: el optimizador asume que nunca ocurre | da la vuelta módulo 2ⁿ (definido) | indefinido | indefinido (SIGFPE en x86) |
| Rust | panic en debug, vuelta en release; checked_/wrapping_/saturating_/overflowing_ son explícitos | igual que con signo | panic en debug | panic |
| Go | da la vuelta (definido) | da la vuelta | 0 / −1, sin panic | panic |
| Java | da la vuelta (definido); Math.addExact lanza | no hay tipos sin signo (char es unsigned de 16 bits) | contador enmascarado a 5/6 bits | ArithmeticException |
| JavaScript | los números son doubles: exactos hasta 2⁵³, luego se redondean; los operadores de bits trabajan sobre int32 | contador enmascarado a 5 bits | Infinity / NaN | |
Las reglas de C que muerden
- Promoción de enteros. Todo lo más pequeño que
intse convierte enintantes de la aritmética, así queuint8_t a = 200, b = 100; a + bes 300, y solo la asignación de vuelta a unuint8_tlo trunca a 44.~xsobre unuint8_tda un int negativo. - Conversiones aritméticas usuales. Cuando un signed se encuentra con un unsigned de la misma anchura, el lado signed se vuelve unsigned:
-1 > 0ues verdadero,if (len - 1 < 0)nunca es verdadero silenes unsize_t. - El desbordamiento con signo es UB.
if (x + 1 < x)como comprobación de desbordamiento lo eliminan GCC y Clang con optimización, porque pueden asumir quex + 1nunca desborda. Comprueba antes de la operación (x > INT_MAX - y) o usa__builtin_add_overflow. - Desplazamientos. Desplazar la anchura o más, o desplazar a la izquierda un valor negativo, es indefinido; x86 enmascara el contador, ARM da cero, el compilador hace lo que diga su modelo.
- División. Trunca hacia cero, así que
-7 / 2 == -3y-7 % 2 == -1;INT_MIN / -1desborda y de hecho provoca un trap en x86.
Leer la salida
El resultado se muestra en el tipo en el que acaba, con su patrón de bits y los mismos bits en la lectura de signo opuesta, es decir, lo que ves cuando un valor cruza un límite (int)/(unsigned) o se guarda a través de otro tipo y se vuelve a leer. La lista de pasos es el orden de evaluación con el tipo en cada paso; las notas son los puntos donde el lenguaje hizo algo distinto de la aritmética simple.
Preguntas
¿Por qué el resultado en C difiere de lo que imprimió mi programa?
Para el comportamiento indefinido no hay una única respuesta correcta: esta página muestra el valor de wraparound que produce el código sin optimizar y marca el paso como UB. Con -O2, el compilador puede haber asumido que el desbordamiento no puede ocurrir y generado otro código. Compila con -fsanitize=undefined para ver el punto exacto.
¿Qué hace el selector de tipo?
Fija el tipo de los literales sin sufijo ni cast, que en C es int por defecto. Elige u8 y escribe 200 + 100 para ver a Rust hacer panic, o a C promocionar a int y tener éxito.
¿Cómo escribo un cast?
Estilo C: (u8) expr, (uint32_t) expr, (int) expr. Estilo Rust: expr as u8. Ambos funcionan en cualquier modo de lenguaje. INT_MAX, INT_MIN, UINT_MAX, LONG_MAX, SIZE_MAX están disponibles como nombres.
¿Maneja 128 bits?
Sí: i128 y u128 se evalúan con exactitud (toda la aritmética se hace con precisión arbitraria y luego se ajusta al tipo).
¿Y la coma flotante?
Aquí no; para eso está el visualizador de floats IEEE 754 de este sitio. Esta herramienta trata solo lo que pasa con los números enteros en tipos de anchura fija.