c++ language-lawyer undefined-behavior

c++ - ¿Es legal que el código fuente que contiene un comportamiento indefinido bloquee el compilador?



language-lawyer undefined-behavior (3)

Digamos que voy a compilar un código fuente de C ++ mal escrito que invoca un comportamiento indefinido, y por lo tanto (como dicen) "cualquier cosa puede pasar".

Desde la perspectiva de lo que la especificación del lenguaje C ++ considera aceptable en un compilador "conforme", "cualquier cosa" en este escenario incluye el bloqueo del compilador (o el robo de mis contraseñas, o de otra manera comportarse mal o fallar en tiempo de compilación), o es ¿El alcance del comportamiento indefinido se limita específicamente a lo que puede suceder cuando se ejecuta el ejecutable resultante?


¿Qué significa "legal" aquí? Cualquier cosa que no contradiga el estándar C o el estándar C ++ es legal, de acuerdo con estos estándares. Si ejecuta una declaración i = i++; y como resultado los dinosaurios se apoderan del mundo, eso no contradice los estándares. Sin embargo, contradice las leyes de la física, por lo que no va a suceder :-)

Si el comportamiento indefinido bloquea su compilador, eso no viola el estándar C o C ++. Sin embargo, significa que la calidad del compilador podría (y probablemente debería) mejorarse.

En versiones anteriores del estándar C, había declaraciones que eran errores o que no dependían de un comportamiento indefinido:

char* p = 1 / 0;

Se permite asignar una constante 0 a un char *. Permitir una constante que no sea cero no lo es. Dado que el valor de 1/0 es un comportamiento indefinido, es un comportamiento indefinido si el compilador debe o no aceptar esta declaración. (Hoy en día, 1/0 ya no cumple con la definición de "expresión constante entera").


La definición normativa del comportamiento indefinido es la siguiente:

[defns.undefined]

comportamiento para el cual esta Norma Internacional no impone requisitos

[Nota: Se puede esperar un comportamiento indefinido cuando esta Norma Internacional omite cualquier definición explícita de comportamiento o cuando un programa utiliza una construcción errónea o datos erróneos. El comportamiento indefinido permitido 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), hasta finalizar una traducción o ejecución (con la emisión de un mensaje de diagnóstico). Muchas construcciones de programa erróneas no generan un comportamiento indefinido; están obligados a ser diagnosticados. La evaluación de una expresión constante nunca exhibe un comportamiento explícitamente especificado como indefinido. - nota final]

Si bien la nota en sí no es normativa, sí describe una variedad de comportamientos que las implementaciones son conocidas por exhibir. Por lo tanto, bloquear el compilador (que es la traducción que termina abruptamente) es legítimo de acuerdo con esa nota. Pero realmente, como dice el texto normativo, el estándar no establece ningún límite ni para la ejecución ni para la traducción. Si una implementación roba sus contraseñas, no es una violación de ningún contrato establecido en el estándar.


La mayoría de los tipos de UB que generalmente nos preocupan, como NULL-deref o dividir por cero, son UB en tiempo de ejecución . Compilar una función que causaría UB en tiempo de ejecución si se ejecuta no debe hacer que el compilador se bloquee. A menos que tal vez pueda probar que la función (y esa ruta a través de la función) definitivamente será ejecutada por el programa.

(Segundo pensamiento: tal vez no he considerado la evaluación requerida de plantilla / constexpr en el momento de la compilación. Posiblemente UB durante eso puede causar rarezas arbitrarias durante la traducción, incluso si la función resultante nunca se llama).

El comportamiento durante la parte de traducción de la cita ISO C ++ en la respuesta de @ StoryTeller es similar al lenguaje utilizado en el estándar ISO C. C no incluye plantillas o constexpr obligatoria constexpr en tiempo de compilación.

Pero hecho curioso: ISO C dice en una nota que si la traducción se termina, debe ser con un mensaje de diagnóstico. O "comportarse durante la traducción ... de manera documentada". No creo que "ignorar la situación por completo" pueda leerse como que incluye detener la traducción.

Antigua respuesta, escrita antes de aprender sobre el tiempo de traducción UB. Sin embargo, es cierto para runtime-UB y, por lo tanto, sigue siendo potencialmente útil.

No existe tal cosa como UB que ocurre en tiempo de compilación. Puede ser visible para el compilador a lo largo de una determinada ruta de ejecución, pero en términos de C ++ no ha sucedido hasta que la ejecución alcanza esa ruta de ejecución a través de una función.

Los defectos en un programa que hacen imposible incluso compilar no son UB, son errores de sintaxis. Dicho programa "no está bien formado" en terminología de C ++ (si tengo mi estándar correcto). Un programa puede estar bien formado pero contener UB. Diferencia entre comportamiento indefinido y mal formado, no se requiere mensaje de diagnóstico

A menos que esté malinterpretando algo, ISO C ++ requiere que este programa se compile y ejecute correctamente, porque la ejecución nunca alcanza la división entre cero. (En la práctica ( Godbolt ), los buenos compiladores solo hacen ejecutables que funcionan. Gcc / clang advierte sobre x / 0 pero no sobre esto, incluso cuando se optimiza. Pero de todos modos, estamos tratando de decir cuán bajo es el ISO C ++ que permite que la calidad de la implementación sea buena. Por lo tanto, verificar gcc / clang no es una prueba útil más que confirmar que escribí el programa correctamente).

int cause_UB() { int x=0; return 1 / x; // UB if ever reached. // Note I''m avoiding x/0 in case that counts as translation time UB. // UB still obvious when optimizing across statements, though. } int main(){ if (0) cause_UB(); }

Un caso de uso para esto podría involucrar el preprocesador C, o constexpr variables constexpr y la ramificación en esas variables, lo que conduce a la constexpr de sentido en algunas rutas que nunca se alcanzan para esas elecciones de constantes.

Se puede suponer que las rutas de ejecución que causan UB visible en tiempo de compilación nunca se toman, por ejemplo, un compilador para x86 podría emitir un ud2 (causa de excepción de instrucción ilegal) como la definición de cause_UB() . O dentro de una función, si un lado de un if() conduce a una UB demostrable , la rama puede eliminarse.

Pero el compilador todavía tiene que compilar todo lo demás de una manera sensata y correcta. Todas las rutas que no se encuentran (o no se puede probar que se encuentren) UB aún deben compilarse en asm que se ejecuta como si la máquina abstracta de C ++ lo estuviera ejecutando.

Se podría argumentar que la UB incondicional en tiempo de compilación visible en main es una excepción a esta regla. O de lo contrario, en tiempo de compilación demostrable, la ejecución que comienza en main de hecho alcanza la UB garantizada.

Todavía argumentaría que los comportamientos legales del compilador incluyen la producción de una granada que explota si se ejecuta. O más plausiblemente, una definición de main que consiste en una sola instrucción ilegal. Yo diría que si nunca ejecutas el programa, todavía no ha habido ninguna UB. El compilador en sí no puede explotar, en mi opinión.

Funciones que contienen UB posibles o comprobables dentro de ramas

UB a lo largo de cualquier ruta de ejecución determinada retrocede a tiempo para "contaminar" todo el código anterior. Pero en la práctica, los compiladores solo pueden aprovechar esa regla cuando pueden demostrar que las rutas de ejecución conducen a una UB visible en tiempo de compilación. p.ej

int minefield(int x) { if (x == 3) { *(char*)nullptr = x/0; } return x * 5; }

El compilador tiene que hacer un asm que funcione para todas las x no sean 3, hasta los puntos donde x * 5 causa UB de desbordamiento con signo en INT_MIN e INT_MAX. Si esta función nunca se llama con x==3 , el programa, por supuesto, no contiene UB y debe funcionar como está escrito.

También podríamos haber escrito if(x == 3) __builtin_unreachable(); en GNU C para decirle al compilador que x definitivamente no es 3.

En la práctica, hay un código de "campo minado" por todos lados en los programas normales. por ejemplo, cualquier división por un entero promete al compilador que no es cero. Cualquier puntero deref promete al compilador que no es NULL.