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 que el compilador falle (o robe mis contraseñas, o se comporte mal o cometa errores en tiempo de compilación), o es alcance del comportamiento indefinido limitado específicamente a lo que puede suceder cuando se ejecuta el ejecutable resultante?
c++
language-lawyer
undefined-behavior
Jeremy Friesner
fuente
fuente
Respuestas:
La definición normativa de comportamiento indefinido es la siguiente:
Si bien la nota en sí no es normativa, describe una variedad de comportamientos que se sabe que exhiben las implementaciones. Por lo tanto, bloquear el compilador (que está terminando la traducción abruptamente) es legítimo según esa nota. Pero realmente, como dice el texto normativo, el estándar no establece límites 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.
fuente
La mayoría de los tipos de UB de los que normalmente nos preocupamos, 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 causar 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 template / constexpr en el momento de la compilación. Posiblemente UB durante eso puede causar rarezas arbitrarias durante la traducción, incluso si nunca se llama a la función resultante).
La parte de comportamiento durante la 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 ni
constexpr
evaluaciones obligatorias en tiempo de compilación.Pero dato curioso: ISO C dice en una nota que si se termina la traducción, 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 interpretarse como una interrupción de la traducción.
Respuesta anterior, escrita antes de que aprendiera sobre la UB en tiempo de traducción. Sin embargo, es cierto para runtime-UB y, por lo tanto, todavía es potencialmente útil.
No existe tal cosa como UB que suceda 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 la compilación no son UB, son errores de sintaxis. Dicho programa "no está bien formado" en terminología C ++ (si tengo mi standardese 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 no entienda algo, ISO C ++ requiere que este programa se compile y se ejecute correctamente, porque la ejecución nunca llega a la división por cero. (En la práctica ( Godbolt ), los buenos compiladores solo crean ejecutables que funcionan. Gcc / clang advierte
x / 0
pero no esto, incluso cuando se optimiza. Pero de todos modos, estamos tratando de decir qué tan bajo ISO C ++ permite que sea la calidad de implementación. Así que verificando gcc / clang no es una prueba útil más que para 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 de C, o las
constexpr
variables y la ramificación en esas variables, lo que conduce a una tontería en algunas rutas que nunca se alcanzan para esas elecciones de constantes.Se puede suponer que las rutas de ejecución que causan UB visibles en tiempo de compilación nunca se toman, por ejemplo, un compilador para x86 podría emitir una
ud2
(causar una excepción de instrucción ilegal) como la definición decause_UB()
. O dentro de una función, si un lado de unif()
conduce a un UB demostrable , la rama se puede quitar.Pero el compilador todavía tiene que compilar todo lo demás de una manera sana y correcta. Todas las rutas que no encuentran (o no se puede probar que encuentren) UB aún deben compilarse en un asm que se ejecuta como si la máquina abstracta de C ++ lo estuviera ejecutando.
Se podría argumentar que la UB incondicional visible en tiempo de compilación en
main
es una excepción a esta regla. O de lo contrario, comprobable en tiempo de compilación que la ejecución que comienza enmain
, de hecho, alcanza UB garantizado.Todavía diría que los comportamientos legales del compilador incluyen producir una granada que explota si se ejecuta. O más plausiblemente, una definición de
main
eso consiste en una sola instrucción ilegal. Yo diría que si nunca ejecuta el programa, todavía no ha habido ningún UB. El compilador en sí no puede explotar, en mi opinión.Funciones que contienen ramas internas de UB posibles o demostrables
UB a lo largo de cualquier ruta de ejecución determinada se remonta en el tiempo para "contaminar" todo el código anterior. Pero en la práctica, los compiladores solo pueden aprovechar esa regla cuando realmente pueden demostrar que las rutas de ejecución conducen a UB visibles 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 todos los
x
demás que no sean 3, hasta los puntos dondex * 5
causa UB de desbordamiento firmado en INT_MIN e INT_MAX. Si esta función nunca se llama conx==3
, el programa, por supuesto, no contiene UB y debe funcionar como está escrito.Bien podríamos haber escrito
if(x == 3) __builtin_unreachable();
en GNU C para decirle al compilador quex
definitivamente no es 3.En la práctica, hay un código de "campo minado" por todas partes en los programas normales. por ejemplo, cualquier división por un número entero promete al compilador que no es cero. Cualquier puntero deref promete al compilador que no es NULL.
fuente
¿Qué significa "legal" aquí? Todo lo 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 un 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 no dependían de un comportamiento indefinido:
char* p = 1 / 0;
Se permite la asignación de una constante 0 a un carácter *. Permitir una constante distinta de 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").
fuente