Indefinido, no especificado e implementación-definida en C

Para entender por qué C es el lenguaje que domina los sistemas de alto rendimiento, hay que entender que no es una lista de reglas restrictivas, sino un contrato entre el programador y el compilador. El programador promete no realizar ciertas acciones prohibidas, y a cambio, el compilador recibe una libertad absoluta para optimizar el código asumiendo que esas prohibiciones nunca se romperán.

Dentro de este contrato, existen tres categorías de comportamientos que no son totalmente deterministas. El comportamiento indefinido (Undefined Behavior o UB) ocurre cuando violas el contrato; en este punto, el estándar no impone ninguna restricción y el compilador puede hacer lo que quiera: desde generar un código que funcione por pura casualidad, hasta eliminar por completo una rama de un if que él considera “imposible” de alcanzar. El comportamiento no especificado (Unspecified Behavior) es simplemente una elección que el compilador puede tomar dentro de un rango de opciones válidas, como el orden en que se evalúan los argumentos de una función; el programa es válido, pero no puedes confiar en el orden. Finalmente, el comportamiento definido por implementación (Implementation-defined Behavior) es cuando el estándar te dice que el compilador debe elegir una opción y, lo más importante, documentarla, como el tamaño de un long o si el tipo char es con signo o sin signo.

Si ignoras el comportamiento indefinido, el resultado es la inestabilidad: tu código puede funcionar en tu máquina con gcc pero fallar catastróficamente al compilar con clang o al subirlo a producción con optimizaciones de nivel -O3.

#include <stdio.h>
#include <limits.h>

/* 
 * Para demostrar el comportamiento no especificado, 
 * usamos una función con efectos secundarios en los argumentos.
 */
void demo_unspecified(int a, int b) {
    printf("  [Unspecified] Argumentos recibidos: a=%d, b=%d\n", a, b);
}

int main(void) {
    printf("--- Inicio de la ejecución ---\n");

    // 1. Implementación-definida: El signo de 'char'
    // El estándar exige que el compilador decida si 'char' es signed o unsigned.
    // Esto afecta cómo se interpreta un valor como 127 + 1.
    char c = 127;
    c = c + 1; 
    printf("[Implementation-defined] Valor de char tras incremento: %d\n", (int)c);

    // 2. No especificado: Orden de evaluación
    // El estándar no dicta el orden en que se evalúan los argumentos de una función.
    int x = 10;
    int y = 20;
    // El orden en que se incrementan x e y antes de pasarlos a la función es indeterminado.
    demo_unspecified(x++, y++);
    printf("  [Unspecified] Valores finales: x=%d, y=%d\n", x, y);

    // 3. Indefinido (UB): Acceso fuera de límites
    // El estándar prohíbe acceder a una posición de memoria fuera de un objeto.
    int buffer[3] = {10, 20, 30};
    int *p = buffer;
    // Acceder al índice 3 en un array de tamaño 3 es UB. 
    // El compilador podría devolver basura o lanzar una señal de segmentación.
    printf("[Undefined] Acceso a buffer[3]: %d\n", p[3]);

    printf("--- Fin de la ejecución ---\n");
    return 0;
}

En el código anterior, la variable c ilustra el comportamiento definido por implementación: si estás en una arquitectura donde char es signed, 127 + 1 desbordará (causando UB si fuera un tipo entero con signo, pero aquí estamos viendo cómo el tipo base cambia la interpretación). El uso de demo_unspecified(x++, y++) muestra el comportamiento no especificado: no hay garantía de que a reciba el valor de x antes o después de que y sea incrementado; el compilador es libre de decidir la secuencia para optimizar el uso de registros. Finalmente, la línea p[3] es comportamiento indefinido; hemos salido de los límites del objeto buffer. A diferencia de los otros casos, aquí el compilador no tiene ninguna obligación de mantener la integridad del programa, y es el escenario donde las optimizaciones de gcc -O3 suelen causar los errores más crípticos.

El error frecuente

Un error clásico de Undefined Behavior es el desbordamiento de enteros con signo (signed integer overflow). A diferencia de los enteros sin signo (unsigned), cuyo desbordamiento está definido por la aritmética modular, desbordar un int es UB.

#include <limits.h>

int desbordamiento_peligroso(int a) {
    return a + 1; // Si a es INT_MAX, esto es UB.
}

Si intentas compilar esto con optimizaciones agresivas, el compilador puede observar que a + 1 nunca será menor que a y, basándose en la premisa de que “el desbordamiento no ocurre”, podría eliminar por completo la comprobación de un if (a + 1 > a). Herramientas como AddressSanitizer (-fsanitize=address) o UndefinedBehaviorSanitizer (-fsanitize=undefined) son esenciales en tu flujo de trabajo para detectar estos casos antes de que lleguen a producción.

75

Dejar un comentario

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

Scroll al inicio