¿Por qué existe el ‘comportamiento indefinido’? [duplicate]

5 minutos de lectura

Ciertos lenguajes de programación comunes, más notablemente C y C++, tienen la fuerte noción de comportamiento indefinido: cuando intenta realizar ciertas operaciones fuera de la forma en que deben usarse, esto provoca un comportamiento indefinido.

Si ocurre un comportamiento indefinido, un compilador puede hacer cualquier cosa (incluyendo nada en absoluto, ‘viajar en el tiempo’, etc.) que quiera.

Mi pregunta es: ¿Por qué existe esta noción de comportamiento indefinido? Por lo que puedo ver, se evitaría una gran cantidad de errores, los programas que funcionan en una versión de un compilador dejan de funcionar en la siguiente, etc., si en lugar de causar un comportamiento indefinido, usar las operaciones fuera de su uso previsto causaría a error de compilación.

¿Por qué no es así como son las cosas?

  • Más o menos esta referencia es la opción para UB: Lo que todo programador de C debe saber sobre UB

    – Mike Vine

    27 de julio de 2018 a las 12:25

  • Por la ideología de la C. Muy flexible y potente dejando todo en manos de los programadores

    – 0___________

    27 de julio de 2018 a las 12:25

  • Interesante charla sobre el tema de Chandler Carruth: Basura entra, basura sale: Discutiendo sobre un comportamiento indefinido…

    – Borglíder

    27 de julio de 2018 a las 12:31

  • “usar las operaciones fuera de su uso previsto causaría una error de compilación” La mayoría de los comportamientos indefinidos en C no son detectables estáticamente, por lo que no pueden ser errores de compilación. Tendrían que ser errores de tiempo de ejecución, lo que implicaría un costo de tiempo de ejecución.

    – sepp2k

    27 de julio de 2018 a las 12:32

  • Aunque es un tema interesante e importante, es demasiado amplio. Ha habido innumerables debates e investigaciones sobre esta pregunta exacta, y todavía hay lenguajes que van desde no tener UB en absoluto, hasta tener UB por todas partes (ejem, C/C++).

    – Transeúnte

    27 de julio de 2018 a las 13:19


avatar de usuario de eerorika
erorika

¿Por qué existe esta noción de comportamiento indefinido?

Permitir que el lenguaje/biblioteca se implemente en una variedad de arquitecturas de computadora diferentes de la manera más eficiente posible (y tal vez en el caso de C, mientras se permite que la implementación siga siendo simple).

si en lugar de causar un comportamiento indefinido, usar las operaciones fuera de su uso previsto provocaría un error de compilación

En la mayoría de los casos de comportamiento indefinido, es imposible (o prohibitivamente costoso en recursos) demostrar que existe un comportamiento indefinido en tiempo de compilación para todos los programas en general.

Alguno los casos son posibles de probar para alguno programas, pero no es posible especificar cuáles de esos casos son exhaustivos, por lo que el estándar no intentará hacerlo. Sin embargo, algunos compiladores son lo suficientemente inteligentes como para reconocer algunos casos simples de UB, y esos compiladores advertirán al programador al respecto. Ejemplo:

int arr[10];
return arr[10];

Este programa tiene un comportamiento indefinido. Una versión particular de GCC que probé muestra:

advertencia: el subíndice 10 de la matriz está por encima de los límites de la matriz de ‘int [10]’ [-Warray-bounds]

No es una buena idea ignorar una advertencia como esta.


Una alternativa más típica a tener un comportamiento indefinido sería tener un manejo de errores definido en tales casos, como lanzar una excepción (compárese, por ejemplo, con Java, donde el acceso a una referencia nula provoca una excepción de tipo java.lang.NullPointerException ser arrojado). Pero verificar las condiciones previas de un comportamiento bien definido es más lento que no verificarlo.

Al no verificar las condiciones previas, el lenguaje le da al programador la opción de probar la corrección por sí mismo y, por lo tanto, evita la sobrecarga de tiempo de ejecución de la verificación en un programa que se demostró que no la necesitaba. De hecho, este poder viene con una gran responsabilidad.

En estos días, la carga de probar que el programa está bien definido se puede aliviar un poco mediante el uso de herramientas (ejemplo) que agregan algunas de esas comprobaciones en tiempo de ejecución y terminan el programa ordenadamente en caso de comprobación fallida.

  • ¡Bien dicho! La eficiencia de la plataforma y la portabilidad del código han influido y moldeado fuertemente a C y C++. Algunas de las mejoras de C++ 11, como la semántica de movimiento, fueron para abordar una deficiencia en la eficiencia potencial. Pero tanto la eficiencia como la portabilidad son difíciles de lograr sin dar a los compiladores mucha libertad de acción… comportamiento indefinido. Otros lenguajes que tienen un comportamiento menos indefinido pueden tener menos rendimiento (a veces mucho menos rendimiento). Es una compensación, y diferentes idiomas tienen diferentes objetivos. Los idiomas son herramientas, adecuadas en su dominio.

    – Eljay

    27 de julio de 2018 a las 12:44

  • Daría +2 si pudiera. Agregaría que los lenguajes más nuevos (versiones de) intentan minimizar el alcance de UB agregando reglas más explícitas al lenguaje (tome Rust y mueva la semántica en C ++ 11, por ejemplo)

    – bartop

    27 de julio de 2018 a las 13:05

  • Otra alternativa común para el comportamiento indefinido es especificar un resultado coherente con el comportamiento natural de muchas plataformas de destino. Por ejemplo, Java especifica que 65535*65537 se ajustará de tal manera que produzca -1, y una expresión de cambio como 1

    – Super gato

    27/07/2018 a las 20:30


  • Un ejemplo clásico de las inconsistencias de la plataforma es el cambio fuera de los límites: auto undef_shift(std::uint32_t v) { return v << 32; }. En algunas arquitecturas, la instrucción de desplazamiento a la izquierda correspondiente atrapa, en otras, siempre devuelve cero (ya que se trata como si estuviera desplazando todos los bits), y en otras, se comportaría igual que return v; porque los bits más altos del segundo operando se ignoran silenciosamente (esto se aplica a x86). Si hubieran exigido algún comportamiento en particular, todas las demás plataformas se verían gravemente penalizadas por el código adicional de verificación/desinfección que el compilador tendría que emitir.

    – Arne Vogel

    28 de julio de 2018 a las 11:10

  • @ArneVogel: Java especifica la reducción mod-32, independientemente de la arquitectura. Las implementaciones de Java en un ARM deben agregar una operación AND si no pueden verificar que el operando está entre 0 y 31. Por otro lado, tener un lenguaje que especifique que el resultado sería una elección no especificada entre x<<(y-1)<<1 y x<<(y & 31) habría permitido una operación eficiente en muchas plataformas y al mismo tiempo permitiría (x<<y)|(x>>(32-y)) ser una forma eficiente de hacer una rotación.

    – Super gato

    28 de julio de 2018 a las 16:37


Avatar de usuario de Michael Kenzel
miguel kenzel

El comportamiento indefinido existe principalmente para dar al compilador la libertad de optimizar. Una cosa que le permite hacer al compilador, por ejemplo, es operar bajo la suposición de que ciertas cosas no pueden suceder (sin tener que probar primero que no pueden suceder, lo que a menudo sería muy difícil o imposible). Al permitirle asumir que ciertas cosas no pueden suceder, el compilador puede entonces eliminar/no tiene que generar código que de otro modo sería necesario para dar cuenta de ciertas posibilidades.

Buena charla sobre el tema.

  • ¿Hay algo en el C89 Justificación u otra documentación de la década de 1980 que respalde ese punto de vista, o es una invención más moderna?

    – Super gato

    27 de julio de 2018 a las 17:11

  • Esta es una buena respuesta. El comportamiento indefinido permite que el compilador simplemente asuma que ciertas cosas nunca suceden y puede optimizarlas en consecuencia. La optimización de una verificación o bifurcación que solo sucedería si hubiera un comportamiento indefinido no sería posible si el compilador se viera obligado a verificar y causar un error en su lugar. Entonces, en cambio, los resultados son ‘impredecibles’, al igual que los resultados de suponer que algo que nunca sucede y luego sucede es impredecible.

    – usuario16217248

    16 ene a las 16:03

El comportamiento indefinido se basa principalmente en el objetivo en el que se pretende ejecutar. El compilador no es responsable del comportamiento dinámico del programa ni del comportamiento estático. Las comprobaciones del compilador están restringidas a las reglas del lenguaje y algunos compiladores modernos también realizan cierto nivel de análisis estático.

Un ejemplo típico serían las variables no inicializadas. Existe debido a las reglas de sintaxis de C, donde una variable puede declararse sin valor inicial. Algunos compiladores asignan 0 a dichas variables y otros simplemente asignan un puntero mem a la variable y lo dejan así. si el programa no inicializa estas variables, conduce a un comportamiento indefinido.

  • Creo que está respondiendo “por qué los compiladores implementan un comportamiento indefinido”, pero la pregunta es “¿por qué el lenguaje estándar implementa un comportamiento indefinido”.

    – VLL

    18 de noviembre de 2022 a las 12:01

  • “Existe debido a las reglas de sintaxis de C”. Esto es falso: “Comportamiento indefinido” no significa cosas que no se mencionan en el estándar. El estándar establece claramente qué comportamientos resultarán en un “comportamiento indefinido”. Los diseñadores del lenguaje ya consideraron cada situación y optaron por convertirla en un “comportamiento indefinido”. Podrían haber definido fácilmente un comportamiento específico para estas situaciones y dejar que los programadores del compilador descubran cómo implementarlo.

    – VLL

    18 de noviembre de 2022 a las 12:01

¿Ha sido útil esta solución?