Conversiones implícitas: Integer Promotions y Conversiones Aritméticas

Cuando escribes una expresión en C, el compilador no opera simplemente con los tipos que declaraste; realiza una serie de transformaciones automáticas para asegurar que las operaciones se realicen entre tipos compatibles. Estas transformaciones se dividen en dos mecanismos: las integer promotions [C11] y las usual arithmetic conversions [C11].

Las integer promotions ocurren cuando un tipo pequeño, como char, short o _Bool, se eleva automáticamente a int (o unsigned int si el valor no cabe en un int con signo) antes de realizar cualquier operación aritmética. Esto sucede porque la CPU suele estar optimizada para trabajar con el tamaño de la palabra del procesador (usualmente 32 o 64 bits), y realizar aritmética de 8 o 16 bits directamente sería ineficiente o requiere instrucciones especiales.

Por otro lado, las usual arithmetic conversions entran en juego en expresiones mixtas (por ejemplo, sumar un int con un float). El compilador busca un “tipo común” siguiendo una jerarquía de rangos. En el caso de tipos enteros, el tipo con el rango más amplio (como long) “gana”. Si los tipos tienen el mismo rango pero uno es con signo (signed) y otro sin signo (unsigned), el tipo con signo se convierte a unsigned. En la jerarquía de punto flotante, el orden es long double > double > float.

Si no entiendes estas reglas, tu código puede compilar sin advertencias pero producir resultados lógicamente erróneos, especialmente al comparar valores negativos con tipos sin signo. Estas reglas existen para garantizar que la aritmética sea determinista y aproveche el hardware, pero su automatización es una trampa clásica si confías en la intuición del lenguaje en lugar de en su especificación.

#include <stdio.h>
#include <stdint.h>

int main(void) {
    // 1. Integer Promotion: char -> int
    // Si char es signed (común en muchas arquitecturas), 
    // un valor como 200 no cabe, pero aquí usaremos un int
    // para ilustrar la promoción de tipos pequeños.
    signed char pequeño = 10;
    int suma_promocion = pequeño + 5; // 'pequeño' se promociona a 'int'

    // 2. Usual Arithmetic Conversion: Enteros de distinto rango
    int i = 100;
    unsigned int u = 200;
    // El 'int' se convierte a 'unsigned int' para coincidir con 'u'
    unsigned int resultado_mixto = i + u;

    // 3. La trampa de la comparación signed vs unsigned
    // Este es el error clásico: el 'int' se convierte a 'unsigned'.
    int negativo = -1;
    unsigned int positivo = 1;
    
    // Aquí ocurre: (unsigned int)-1, que es el valor máximo posible.
    // Por tanto, (-1u < 1u) es falso.
    int es_menor = (negativo < positivo);

    // 4. Hierarquía de punto flotante
    float f = 1.5f;
    double d = 2.0;
    // 'f' se convierte a 'double' antes de la suma
    double resultado_float = f + d;

    printf("Promocion: %d\n", suma_promocion);
    printf("Mixto: %u\n", resultado_mixto);
    printf("¿Es -1 < 1u?: %s\n", es_menor ? "Verdadero" : "Falso");
    printf("Float + Double: %f\n", resultado_float);

    return 0;
}

Análisis del comportamiento

Fíjate en la variable pequeño. Aunque es de tipo signed char, al sumarse con 5 (que es un literal int), el compilador aplica una integer promotion. Internamente, la CPU no suma un byte con un entero, sino que carga el valor en un registro de 32 bits y realiza la operación como si pequeño fuera un int.

En la expresión i + u, tenemos un int y un unsigned int. Según las reglas de las usual arithmetic conversions, para que ambos tengan el mismo tipo, el int debe convertirse a unsigned int. Esto es seguro mientras i sea positivo, pero peligroso si i fuera negativo.

El caso de negativo < positivo es donde la lógica humana suele fallar. Como positivo es unsigned int, la expresión negativo < positivo obliga a que negativo (un -1) se convierta a unsigned int. En la representación binaria (complemento a dos), -1 representado como un unsigned int es el valor máximo representable (ej. 4294967295 en 32 bits). Por eso, la comparación devuelve 0 (falso): 4294967295 < 1 es falso.

Finalmente, en f + d, la jerarquía dicta que el float tiene menor rango que el double. Para evitar la pérdida de precisión que ocurriría si convirtiéramos el double a float, el compilador promociona el float a double antes de realizar la suma.

El error frecuente

Un error muy común en sistemas de alto rendimiento ocurre al usar size_t (que es unsigned) para comparar tamaños de memoria o índices de arrays frente a una variable int que puede ser negativa.

// Código erróneo
int offset = -5;
size_t limite = 10;

if (offset < limite) {
    // Esto NUNCA se ejecutará como esperas
}

En este caso, offset se convierte a size_t. Como -5 convertido a un tipo sin signo es un número masivo, la condición (unsigned)-5 < 10 resulta ser falsa. Esto suele pasar desapercibido porque no hay un error de sintaxis. Para detectar esto, es imprescindible compilar con la bandera -Wsign-compare en GCC o Clang, o utilizar herramientas como AddressSanitizer para detectar desbordamientos de memoria resultantes de estas comparaciones fallidas.

30

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio