c++ c language-lawyer undefined-behavior

c++ - ¿El "comportamiento indefinido" realmente permite que suceda*algo*?



language-lawyer undefined-behavior (9)

Los comportamientos indefinidos permiten a los compiladores generar código más rápido en algunos casos. Considere dos arquitecturas de procesador diferentes que AGREGAN de manera diferente: el procesador A descarta inherentemente el bit de transferencia al desbordarse, mientras que el procesador B genera un error. (Por supuesto, el procesador C genera inherentemente demonios nasales, es la forma más fácil de descargar esa cantidad extra de energía en un nanobot con mocos ...)

Si el estándar requiere que se genere un error, entonces todo el código compilado para el procesador A básicamente se vería obligado a incluir instrucciones adicionales, a realizar algún tipo de verificación de desbordamiento y, de ser así, generar un error. Esto daría como resultado un código más lento, incluso si el desarrollador sabe que solo terminarán agregando números pequeños.

El comportamiento indefinido sacrifica la portabilidad por la velocidad. Al permitir que suceda "cualquier cosa", el compilador puede evitar escribir controles de seguridad para situaciones que nunca ocurrirán. (O, ya sabes ... podrían).

Además, cuando un programador sabe exactamente qué causará realmente un comportamiento indefinido en su entorno dado, puede explotar ese conocimiento para obtener un rendimiento adicional.

Si desea asegurarse de que su código se comporte exactamente igual en todas las plataformas, debe asegurarse de que nunca ocurra un ''comportamiento indefinido''; sin embargo, este puede no ser su objetivo.

Editar: (en respuesta a los OP editar) Implementación El comportamiento definido requeriría la generación constante de demonios nasales. El comportamiento indefinido permite la generación esporádica de demonios nasales.

Ahí es donde aparece la ventaja que tiene el comportamiento indefinido sobre el comportamiento específico de implementación. Tenga en cuenta que puede ser necesario un código adicional para evitar comportamientos inconsistentes en un sistema en particular. En estos casos, el comportamiento indefinido permite una mayor velocidad.

Esta pregunta ya tiene una respuesta aquí:

EDITAR: Esta pregunta no pretendía ser un foro de discusión sobre los (de) méritos del comportamiento indefinido, pero eso es algo en lo que se convirtió. En cualquier caso, este hilo sobre un hipotético compilador de C sin comportamiento indefinido puede ser de interés adicional para aquellos que piensan que este es un tema importante.

El clásico ejemplo apócrifo de "comportamiento indefinido" es, por supuesto, "demonios nasales", una imposibilidad física, independientemente de lo que permitan los estándares C y C ++.

Debido a que las comunidades C y C ++ tienden a poner tanto énfasis en la imprevisibilidad del comportamiento indefinido y la idea de que el compilador puede hacer que el programa haga literalmente cualquier cosa cuando se encuentra un comportamiento indefinido, supuse que el estándar no impone restricciones de ningún tipo sobre el comportamiento de, bueno, comportamiento indefinido.

Pero la cita relevante en el estándar C ++ parece ser :

[C++14: defns.undefined]: [..] El comportamiento indefinido [C++14: defns.undefined]: varía desde ignorar la situación por completo con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), para finalizar una traducción o ejecución (con la emisión de un mensaje de diagnóstico). [..]

En realidad, esto especifica un pequeño conjunto de opciones posibles:

  • Ignorando la situación : sí, el estándar continúa diciendo que esto tendrá "resultados impredecibles", pero eso no es lo mismo que el código de inserción del compilador (que supongo que sería un requisito previo para, ya sabes, demonios nasales).
  • Comportarse de manera documentada, característica del medio ambiente , esto en realidad suena relativamente benigno. (Ciertamente no he oído hablar de ningún caso documentado de demonios nasales).
  • Terminar la traducción o ejecución , con un diagnóstico, nada menos. Ojalá todo UB se comportara tan bien.

Supongo que en la mayoría de los casos, los compiladores eligen ignorar el comportamiento indefinido; por ejemplo, cuando se lee memoria no inicializada, presumiblemente sería una anti-optimización insertar cualquier código para asegurar un comportamiento consistente. Supongo que los tipos más extraños de comportamiento indefinido (como el " viaje en el tiempo ") entrarían en la segunda categoría, pero esto requiere que tales comportamientos estén documentados y sean "característicos del entorno" (así que supongo que los demonios nasales solo son producidos por computadoras infernales?

¿Estoy malinterpretando la definición? ¿Se pretende que sean simples ejemplos de lo que podría constituir un comportamiento indefinido, en lugar de una lista completa de opciones? ¿Es la afirmación de que "cualquier cosa puede pasar" significa simplemente como un efecto secundario inesperado de ignorar la situación?

EDITAR: Dos puntos menores de aclaración:

  • Pensé que estaba claro por la pregunta original, y creo que para la mayoría de la gente lo era, pero lo explicaré de todos modos: me doy cuenta de que los "demonios nasales" son irónicos.
  • No escriba una (otra) respuesta explicando que UB permite optimizaciones de compilador específicas de la plataforma, a menos que también explique cómo permite optimizaciones que el comportamiento definido por la implementación no permitiría.

El comportamiento indefinido es simplemente el resultado de una situación que los escritores de la especificación no previeron.

Tome la idea de un semáforo. Rojo significa detenerse, amarillo significa prepararse para el rojo y verde significa ir. En este ejemplo, las personas que conducen automóviles son la implementación de la especificación.

¿Qué sucede si tanto el verde como el rojo están encendidos? ¿Te detienes y luego te vas? ¿Esperas hasta que el rojo se apaga y es solo verde? Este es un caso que la especificación no describió y, como resultado, todo lo que hacen los controladores es un comportamiento indefinido. Algunas personas harán una cosa, algunas otras. Como no hay garantía sobre lo que sucederá, desea evitar esta situación. Lo mismo se aplica al código.


La definición de comportamiento indefinido, en cada estándar C y C ++, es esencialmente que el estándar no impone requisitos sobre lo que sucede.

Sí, eso significa que se permite cualquier resultado. Pero no hay resultados particulares que se requieran para suceder, ni ningún resultado que NO se requiera . No importa si tiene un compilador y una biblioteca que constantemente producen un comportamiento particular en respuesta a una instancia particular de comportamiento indefinido, tal comportamiento no es necesario y puede cambiar incluso en una futura versión de corrección de errores de su compilador, y el compilador seguirá siendo perfectamente correcto según cada versión de los estándares C y C ++.

Si su sistema host tiene soporte de hardware en forma de conexión a sondas que se insertan en sus fosas nasales, es posible que un comportamiento indefinido cause efectos nasales no deseados.


Pensé que respondería solo uno de sus puntos, ya que las otras respuestas responden bastante bien a la pregunta general, pero lo he dejado sin abordar.

"Ignorando la situación: sí, el estándar continúa diciendo que esto tendrá" resultados impredecibles ", pero eso no es lo mismo que el código de inserción del compilador (que supongo que sería un requisito previo para, ya sabes, demonios nasales). "

Una situación en la que podría esperarse que los demonios nasales ocurrieran razonablemente con un compilador sensato, sin que el compilador inserte CUALQUIER código, sería la siguiente:

if(!spawn_of_satan) printf("Random debug value: %i/n", *x); // oops, null pointer deference nasal_angels(); else nasal_demons();

Un compilador, si puede probar que * x es una desreferencia de puntero nulo, tiene el derecho perfecto, como parte de alguna optimización, a decir "OK, así que veo que han desreferenciado un puntero nulo en esta rama del if. Por lo tanto, como parte de esa rama, puedo hacer cualquier cosa. Por lo tanto, puedo optimizar esto: "

if(!spawn_of_satan) nasal_demons(); else nasal_demons();

"Y a partir de ahí, puedo optimizar esto:"

nasal_demons();

Puede ver cómo este tipo de cosas en las circunstancias correctas puede resultar muy útil para un compilador de optimización y, sin embargo, causar un desastre. Hace un tiempo vi algunos ejemplos de casos en los que en realidad ES importante para la optimización poder optimizar este tipo de casos. Podría tratar de desenterrarlos más tarde cuando tenga más tiempo.

EDITAR: Un ejemplo que acaba de llegar de las profundidades de mi memoria de un caso en el que es útil para la optimización es donde con frecuencia se verifica que un puntero sea NULO (tal vez en funciones auxiliares en línea), incluso después de haberlo desreferenciado y sin tener lo cambió El compilador de optimización puede ver que lo ha desreferenciado y, por lo tanto, optimizar todas las comprobaciones de "es NULO", ya que si lo ha desreferenciado y ES nulo, se permite que suceda cualquier cosa, incluso no ejecutar el "es NULO" cheques. Creo que argumentos similares se aplican a otros comportamientos indefinidos.


Primero, es importante tener en cuenta que no solo el comportamiento del programa de usuario es indefinido, sino también el comportamiento del compilador . Del mismo modo, UB no se encuentra en tiempo de ejecución, es una propiedad del código fuente.

Para un escritor compilador, "el comportamiento es indefinido" significa "no tiene que tener en cuenta esta situación", o incluso "puede asumir que ningún código fuente producirá esta situación". Un compilador puede hacer cualquier cosa, intencional o involuntariamente, cuando se le presenta con UB, y aún cumplir con la norma, por lo que sí, si le otorgó acceso a su nariz ...

Entonces, no siempre es posible saber si un programa tiene UB o no. Ejemplo:

int * ptr = calculateAddress(); int i = *ptr;

Saber si esto puede ser UB o no requeriría conocer todos los valores posibles devueltos por calculateAddress() , lo cual es imposible en el caso general (consulte " Problema de detención "). Un compilador tiene dos opciones:

  • suponga que ptr siempre tendrá una dirección válida
  • inserte controles de tiempo de ejecución para garantizar un cierto comportamiento

La primera opción produce programas rápidos y pone la carga de evitar efectos no deseados en el programador, mientras que la segunda opción produce un código más seguro pero más lento.

Los estándares C y C ++ dejan esta opción abierta, y la mayoría de los compiladores eligen el primero, mientras que Java, por ejemplo, exige el segundo.

¿Por qué el comportamiento no está definido por la implementación, sino indefinido?

Medios definidos por la implementación ( N4296 , 1.9§2):

Ciertos aspectos y operaciones de la máquina abstracta se describen en esta Norma Internacional como definidos por la implementación (por ejemplo, sizeof (int)). Estos constituyen los parámetros de la máquina abstracta. Cada implementación debe incluir documentación que describa sus características y comportamiento en estos aspectos. Dicha documentación definirá la instancia de la máquina abstracta que corresponde a esa implementación (denominada "instancia correspondiente" a continuación).

El énfasis es mío. En otras palabras: un compilador-escritor tiene que documentar exactamente cómo se comporta el código de máquina, cuando el código fuente usa características definidas por la implementación.

Escribir en un puntero no válido aleatorio no nulo es una de las cosas más impredecibles que puede hacer en un programa, por lo que esto también requeriría verificaciones de tiempo de ejecución que reducen el rendimiento.
Antes de que tuviéramos MMU, podías destruir el hardware escribiendo en la dirección incorrecta, que se acerca mucho a los demonios nasales ;-)


Sí, permite que pase cualquier cosa. La nota solo da ejemplos. La definición es bastante clara:

Comportamiento indefinido: comportamiento para el cual esta Norma Internacional no impone requisitos.

Punto de confusión frecuente:

¡Debe entender que "ningún requisito" también significa que la implementación NO es necesaria para dejar el comportamiento indefinido o hacer algo extraño / no determinista!

La implementación está perfectamente permitida por el estándar C ++ para documentar un comportamiento sensato y comportarse en consecuencia. 1 Entonces, si su compilador afirma que se desborda en un desbordamiento firmado, la lógica (¿cordura?) Dictaría que puede confiar en ese comportamiento en ese compilador . Simplemente no esperes que otro compilador se comporte de la misma manera si no lo dice.

1 Diablos, incluso está permitido documentar una cosa y hacer otra. Eso sería estúpido, y probablemente te haría tirarlo a la basura, ¿por qué confiarías en un compilador cuya documentación te pertenece? Pero no está en contra del estándar C ++.


Una de las razones para dejar el comportamiento indefinido es permitir que el compilador haga cualquier suposición que desee al optimizar.

Si existe alguna condición que debe cumplirse si se va a aplicar una optimización, y esa condición depende de un comportamiento indefinido en el código, entonces el compilador puede asumir que se cumple, ya que un programa conforme no puede depender del comportamiento indefinido en ningún camino. Es importante destacar que el compilador no necesita ser coherente en estos supuestos. (que no es el caso para el comportamiento definido por la implementación)

Supongamos que su código contiene un ejemplo artificialmente inventado como el siguiente:

int bar = 0; int foo = (undefined behavior of some kind); if (foo) { f(); bar = 1; } if (!foo) { g(); bar = 1; } assert(1 == bar);

El compilador es libre de asumir que! Foo es verdadero en el primer bloque y foo es verdadero en el segundo, y así optimizar todo el fragmento de código. Ahora, lógicamente, foo o! Foo deben ser verdaderos, por lo que si observa el código, razonablemente podrá suponer que la barra debe ser igual a 1 una vez que haya ejecutado el código. Pero debido a que el compilador se optimizó de esa manera, la barra nunca se establece en 1. Y ahora esa afirmación se vuelve falsa y el programa termina, que es un comportamiento que no habría sucedido si foo no hubiera confiado en un comportamiento indefinido.

Ahora, ¿es posible que el compilador realmente inserte código completamente nuevo si ve un comportamiento indefinido? Si hacerlo le permitirá optimizar más, absolutamente. ¿Es probable que suceda con frecuencia? Probablemente no, pero nunca puedes garantizarlo, por lo que operar bajo la suposición de que los demonios nasales son posibles es el único enfoque seguro.


Uno de los propósitos históricos de Comportamiento indefinido era permitir la posibilidad de que ciertas acciones puedan tener diferentes efectos potencialmente útiles en diferentes plataformas. Por ejemplo, en los primeros días de C, dado

int i=INT_MAX; i++; printf("%d",i);

algunos compiladores pueden garantizar que el código imprima algún valor particular (para una máquina con dos complementos, típicamente sería INT_MIN), mientras que otros garantizarían que el programa finalizaría sin llegar a printf. Dependiendo de los requisitos de la aplicación, cualquier comportamiento podría ser útil. Dejar el comportamiento indefinido significaba que una aplicación en la que la terminación anormal del programa era una consecuencia aceptable del desbordamiento, pero no produciría una salida aparentemente válida pero incorrecta, podría renunciar a la comprobación del desbordamiento si se ejecutaba en una plataforma que lo atraparía de manera confiable, y una aplicación donde la terminación anormal en caso de desbordamiento no sería aceptable, pero la producción de resultados aritméticamente incorrectos sí lo sería, podría renunciar a la verificación de desbordamiento si se ejecuta en una plataforma donde los desbordamientos no quedaron atrapados.

Recientemente, sin embargo, algunos autores de compiladores parecen haberse metido en un concurso para ver quién puede eliminar de manera más eficiente cualquier código cuya existencia no sea obligatoria por el estándar. Dado, por ejemplo ...

#include <stdio.h> int main(void) { int ch = getchar(); if (ch < 74) printf("Hey there!"); else printf("%d",ch*ch*ch*ch*ch); }

un compilador hipermoderno puede concluir que si ch es 74 o mayor, el cálculo de ch*ch*ch*ch*ch produciría un Comportamiento indefinido y, como consecuencia, el programa debería imprimir "¡Hola!" incondicionalmente, independientemente del personaje que se haya escrito.


Nitpicking : no has citado un estándar.

Estas son las fuentes utilizadas para generar borradores del estándar C ++. Estas fuentes no deben considerarse una publicación ISO, ni los documentos generados a partir de ellas, a menos que sean oficialmente adoptadas por el grupo de trabajo C ++ (ISO / IEC JTC1 / SC22 / WG21).

Interpretación : Las notas no son normative acuerdo con las Directivas ISO / IEC Parte 2.

Las notas y ejemplos integrados en el texto de un documento solo se utilizarán para proporcionar información adicional destinada a ayudar a la comprensión o el uso del documento. No deben contener requisitos ("debe"; ver 3.3.1 y Tabla H.1) o cualquier información considerada indispensable para el uso del documento, por ejemplo, instrucciones (imperativo; ver Tabla H.1), recomendaciones ("debería"; ver 3.3.2 y Tabla H.2) o permiso ("puede"; ver Tabla H.3). Las notas pueden escribirse como una declaración de hechos.

El énfasis es mío. Esto solo descarta la "lista completa de opciones". Sin embargo, dar ejemplos cuenta como "información adicional destinada a ayudar a la comprensión del documento".

Tenga en cuenta que el meme del "demonio nasal" no debe tomarse literalmente, al igual que el uso de un globo para explicar cómo funciona la expansión del universo no es verdad en la realidad física. Es para ilustrar que es imprudente discutir qué "comportamiento indefinido" debe hacer cuando está permitido hacer algo. Sí, esto significa que no hay una banda elástica real en el espacio exterior.