c++ language-lawyer undefined-behavior

c++ - ¿Una expresión con comportamiento indefinido que nunca se ejecuta realmente hace que un programa sea erróneo?



language-lawyer undefined-behavior (8)

En muchas discusiones sobre el comportamiento indefinido (UB), se ha expresado el punto de vista de que en la mera presencia en un programa de cualquier construcción que tenga UB en un programa exige una implementación conforme para hacer cualquier cosa (sin incluir nada). Mi pregunta es si esto debería tomarse en ese sentido incluso en aquellos casos en que la UB está asociada a la ejecución del código, mientras que el comportamiento (de lo contrario) especificado en el estándar estipula que el código en cuestión no debe ejecutarse (y esto posiblemente para una entrada específica al programa; es posible que no se pueda decidir en el momento de la compilación).

Fraseado de manera más informal, el olor de UB exige una implementación conforme para decidir que todo el programa apesta, y se niega a ejecutar correctamente incluso las partes del programa para las cuales el comportamiento está perfectamente definido. Un programa ejemplo sería

#include <iostream> int main() { int n = 0; if (false) n=n++; // Undefined behaviour if it gets executed, which it doesn''t std::cout << "Hi there./n"; }

Para mayor claridad, asumo que el programa está bien formado (por lo tanto, en particular, la UB no está asociada al preprocesamiento). De hecho, estoy dispuesto a restringir a UB asociado a "evaluaciones", que claramente no son entidades de tiempo de compilación. Las definiciones pertinentes al ejemplo dado son, creo, (el énfasis es mío):

La secuencia anterior es una relación asimétrica, transitiva y por pares entre las evaluaciones ejecutadas por un solo hilo (1.10), que induce un orden parcial entre esas evaluaciones

Los cálculos de valores de los operandos de un operador se secuencian antes del cálculo de valores del resultado del operador. Si un efecto secundario en un objeto escalar no tiene secuencia en relación con ... o un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.

Queda implícitamente claro que los sujetos de la oración final, "efecto secundario" y "cálculo de valor", son instancias de "evaluación", ya que para eso se define la relación "secuenciada antes".

Creo que en el programa anterior, el estándar estipula que no se realizan evaluaciones para las que se cumple la condición en la oración final (sin secuencia entre ellas y del tipo descrito) y que, por lo tanto, el programa no tiene UB; no es erróneo

En otras palabras, estoy convencido de que la respuesta a la pregunta de mi título es negativa. Sin embargo, apreciaría las opiniones (motivadas) de otras personas sobre este asunto.

Tal vez una pregunta adicional para aquellos que abogan por una respuesta afirmativa, ¿exigiría que el cambio de formato proverbial de su disco duro pueda ocurrir cuando se compila un programa erróneo?

Algunos punteros relacionados en este sitio:

  • Comportamiento observable y comportamiento indefinido: ¿qué sucede si no llamo un destructor?
  • Comentarios a esta respuesta https://stackoverflow.com/a/24143792/1436796 (ya no aguanto absolutamente mi respuesta)
  • C ++ ¿Cuál es el comportamiento indefinido más temprano que puede manifestarse?
  • Diferencia entre comportamiento indefinido y mal formado, no se requiere mensaje de diagnóstico y sus dos respuestas, que representan puntos de vista opuestos

Si un efecto secundario en un objeto escalar no tiene secuencia en relación a etc.

Los efectos secundarios son cambios en el estado del entorno de ejecución (1.9 / 12). Un cambio es un cambio, no una expresión que, si se evalúa, podría producir un cambio . Si no hay cambio, no hay efecto secundario. Si no hay ningún efecto secundario, entonces no hay efecto secundario sin secuencia en relación con cualquier otra cosa.

Esto no significa que ningún código que nunca se ejecute esté libre de UB (aunque estoy bastante seguro de que la mayoría lo es). Cada aparición de UB en la norma debe examinarse por separado. (El texto eliminado es probablemente demasiado cauteloso; vea más abajo).

La norma también dice que

Una implementación conforme que ejecuta un programa bien formado producirá el mismo comportamiento observable que una de las ejecuciones posibles de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada. Sin embargo, si alguna de estas ejecuciones contiene una operación indefinida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación no definida).

(énfasis mío)

Esto, por lo que puedo decir, es la única referencia normativa que dice lo que la frase "comportamiento indefinido" significa: una operación indefinida en la ejecución de un programa . Sin ejecución, sin UB.


Debe ser, si no " deberá ".

El comportamiento , por definición de ISO C (no se encuentra una definición correspondiente en ISO C ++ pero aún debería ser aplicable de alguna manera), es:

3.4

1 comportamiento

Apariencia externa o acción

Y UB:

WG21 / N4527

1.3.25 [defns.undefined]

comportamiento indefinido

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 o datos erróneos erróneos. El comportamiento indefinido permisible va desde ignorar la situación completamente con resultados impredecibles, a 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 terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico). Muchas construcciones de programas erróneas no generan un comportamiento indefinido; Se requieren para ser diagnosticados. "Nota final"

A pesar de "comportarse durante la traducción" más arriba, la palabra "comportamiento" utilizada por ISO C ++ se refiere principalmente a la ejecución de programas.

WG21 / N4527

1.9 Ejecución del programa [ejecución intro.]

1 Las descripciones semánticas en esta Norma Internacional definen una máquina abstracta no determinista parametrizada. Esta norma internacional no impone ningún requisito a la estructura de las implementaciones conformes. En particular, no es necesario que copien o emulen la estructura de la máquina abstracta. Más bien, se requiere que las implementaciones conformes emulen (solo) el comportamiento observable de la máquina abstracta como se explica a continuación.5

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 a este respecto. 6 Dicha documentación debe definir la instancia de la máquina abstracta que corresponde a esa implementación (denominada "instancia correspondiente" a continuación).

3 Ciertos otros aspectos y operaciones de la máquina abstracta se describen en esta Norma Internacional como no especificados (por ejemplo, evaluación de expresiones en un nuevo inicializador si la función de asignación no puede asignar memoria (5.3.4)). Donde sea posible, esta norma internacional define un conjunto de comportamientos permitidos. Estos definen los aspectos no deterministas de la máquina abstracta. Una instancia de la máquina abstracta puede tener más de una ejecución posible para un programa dado y una entrada dada.

4 Ciertas otras operaciones se describen en esta Norma Internacional como indefinidas (por ejemplo, el efecto de intentar modificar un objeto const ). [Nota: esta norma internacional no impone requisitos sobre el comportamiento de los programas que contienen un comportamiento indefinido. "Nota final"

5 Una implementación conforme que ejecuta un programa bien formado producirá el mismo comportamiento observable que una de las ejecuciones posibles de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada. Sin embargo, si alguna de estas ejecuciones contiene una operación indefinida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación no definida).

5) Esta disposición a veces se denomina regla "como si", porque una implementación es libre de ignorar cualquier requisito de esta Norma Internacional siempre que el resultado sea como si el requisito hubiera sido obedecido, en la medida en que se pueda determinar a partir de Comportamiento observable del programa. Por ejemplo, una implementación real no necesita evaluar parte de una expresión si puede deducir que su valor no se usa y que no se producen efectos secundarios que afecten el comportamiento observable del programa.

6) Esta documentación también incluye construcciones soportadas condicionalmente y comportamiento específico de la configuración regional. Ver 1.4.

Está claro que el comportamiento indefinido podría ser causado por una construcción de lenguaje específica utilizada de forma incorrecta o no portátil (que no cumple con la norma). Sin embargo, el estándar no menciona nada sobre qué parte específica del código en un programa lo causaría. En otras palabras, "tener un comportamiento indefinido" es la propiedad (sobre la conformidad) de todo el programa que se está ejecutando, no una parte más pequeña del mismo .

El estándar podría haber dado una mayor garantía para que el comportamiento esté bien definido una vez que no se está ejecutando algún código específico, solo cuando existe una manera de asignar el código C ++ al comportamiento correspondiente de manera precisa . Esto es difícil (si no imposible) sin un modelo semántico detallado sobre la ejecución. En resumen, la semántica operacional dada por el modelo de máquina abstracta anterior no es suficiente para lograr una garantía más sólida . Pero de todos modos, ISO C ++ nunca sería JVMS o ECMA-335. Y no espero que haya un conjunto completo de semántica formal que describa el lenguaje.

Un problema clave aquí es el significado de "ejecución". Algunas personas piensan que "ejecutar un programa" significa hacer que el programa se ejecute. Esto no es completamente cierto. Tenga en cuenta que la representación del programa ejecutado en la máquina abstracta no está especificada. (También tenga en cuenta que "esta Norma Internacional no impone requisitos en la estructura de las implementaciones conformes".) El código que se ejecuta aquí puede ser literalmente código C ++ (no necesariamente código de máquina o alguna otra forma de código intermedio que no está especificado en absoluto por el estándar). ). Esto permite que el lenguaje central sea implementado como un intérprete, un evaluador parcial en línea o algunos otros monstruos que traducen el código C ++ sobre la marcha. Como resultado, en realidad no hay forma de dividir las fases de traducción (definidas por ISO C ++ [lex.phases]) completamente por delante del proceso de ejecución sin el conocimiento de implementaciones específicas. Por lo tanto, es necesario permitir que ocurra UB durante la traducción cuando es demasiado difícil especificar un comportamiento portátil bien definido.

Además de los problemas anteriores, quizás para la mayoría de los usuarios normales, una razón (no técnica) es suficiente: simplemente no es necesario proporcionar la garantía más fuerte, permitir el código incorrecto y anular uno de los aspectos de utilidad (probablemente el más importante) de la propia UB: para alentar a que desechen rápidamente algunos códigos malolientes no innecesarios (innecesariamente) sin el esfuerzo de "arreglarlos", lo que eventualmente sería en vano.

Notas adicionales:

Algunas palabras se copian y se reconstruyen de una de mis respuestas a este comentario .


El compilador de CA puede hacer lo que quiera cuando un programa entra en un estado a través del cual no hay una secuencia definida de eventos que le permita al programa evitar invocar un comportamiento indefinido en algún momento en el futuro (tenga en cuenta cualquier ciclo que no tenga cualquier efecto secundario, y que no tenga una condición de salida que un compilador tendría que reconocer, invoca un comportamiento indefinido en sí mismo. El comportamiento del compilador en tales casos no está sujeto a las leyes del tiempo ni de la causalidad . En situaciones donde el comportamiento indefinido ocurre en una expresión cuyo resultado nunca se usa, algunos compiladores no generarán ningún código para la expresión (por lo que nunca se "ejecutará") pero eso no evitará que los compiladores usen el comportamiento indefinido para hacer que otros inferencias sobre el comportamiento del programa.

Por ejemplo:

void maybe_launch_missiles(void) { if (should_launch_missiles()) { arm_missiles(); if (should_launch_missiles()) launch_missiles(); } disarm_missiles(); } int foo(int x) { maybe_launch_missiles(); return x<<1; }

Bajo el estándar C actual, si el compilador pudiera determinar que disarm_missiles() siempre regresaría sin terminar pero las otras tres funciones externas llamadas arriba podrían terminar, el reemplazo más eficiente que cumple con el estándar para la declaración foo(-1); (el valor de retorno ignorado) sería should_launch_missiles(); arm_missiles(); should_launch_missiles(); launch_missiles(); should_launch_missiles(); arm_missiles(); should_launch_missiles(); launch_missiles(); .

El comportamiento del programa solo se definirá si la llamada a should_launch_missiles() termina sin regresar, si la primera llamada devuelve no cero y arm_missiles() termina sin regresar, o si ambas llamadas devuelven no cero y launch_missiles() termina sin regresar. Un programa que funcione correctamente en esos casos cumplirá con el estándar independientemente de lo que haga en cualquier otra situación. Si devolver desde maybe_launch_missiles() causaría un comportamiento indefinido, no se requerirá que el compilador reconozca la posibilidad de que cualquiera de las llamadas a should_launch_missiles() pueda devolver cero.

Como consecuencia, algunos compiladores modernos, el efecto de desplazar a la izquierda un número negativo puede ser peor que cualquier cosa que pueda ser causada por cualquier tipo de comportamiento indefinido en un compilador C99 típico en plataformas que separan los espacios de datos y códigos y el desbordamiento de la pila de captura. Incluso si el código involucrado en un comportamiento indefinido que podría causar transferencias de control aleatorias, no habría ningún medio por el cual pudiera hacer que arm_missiles() y launch_missiles() se llamen consecutivamente sin una llamada intermedia para disarm_missiles() menos que al menos una llamada a should_launch_missiles() devolvió un valor distinto de cero. Un compilador hiper-moderno, sin embargo, puede negar tales protecciones.


En el caso general lo mejor que podemos decir aquí es que depende.

Un caso donde la respuesta es no, ocurre cuando se trata de valores indeterminados. El último borrador claramente hace que su comportamiento indefinido produzca un valor indeterminado durante una evaluación con algunas excepciones, pero el ejemplo del código muestra claramente lo sutil que podría ser:

[ Ejemplo:

int f(bool b) { unsigned char c; unsigned char d = c; // OK, d has an indeterminate value int e = d; // undefined behavior return b ? d : 0; // undefined behavior if b is true }

- ejemplo final ]

Así que esta línea de código:

return b ? d : 0;

sólo está indefinido si b es true . Este parece ser el enfoque intuitivo y parece ser el modo en que John Regehr también lo ve, si leemos que es hora de tomar en serio la explotación de un comportamiento indefinido .

En este caso, la respuesta es sí, el código es erróneo, aunque no estamos llamando al código que invoca un comportamiento indefinido:

constexpr const char *str = "Hello World" ; constexpr char access() { return str[100] ; } int main() { }

clang elige hacer que el access erróneo, aunque nunca se invoque ( verlo en vivo ).


En el contexto de un sistema integrado de seguridad crítico, el código publicado se consideraría defectuoso:

  1. El código no debe pasar la revisión del código y / o el cumplimiento de las normas (MISRA, etc.)
  2. El análisis estático (lint, cppcheck, etc.) debe marcar esto como un defecto
  3. Algunos compiladores pueden marcar esto como una advertencia (lo que también implica un defecto).

Existe una clara división entre el comportamiento indefinido inherente, como n = n ++, y el código que puede tener un comportamiento definido o no definido dependiendo del estado del programa en tiempo de ejecución, como x / y para ints. En este último caso, se requiere que el programa funcione a menos que y sea 0, pero en el primer caso, el compilador le pide que genere un código que es totalmente ilegítimo. Está dentro de sus derechos rechazar la compilación, puede que no esté "a prueba de balas" contra tales el código y, por consiguiente, su estado optimizador (registro de asignaciones, registros de qué valores pueden haber sido modificados desde que se leyó, etc.) se corrompe y se obtiene un código de máquina falso para eso y el código fuente circundante . Puede ser que el análisis temprano reconociera una situación "a = b ++" y generara un código para el anterior si saltar sobre una instrucción de dos bytes, pero cuando se encuentra n = n ++, no se emitió ninguna instrucción, de modo que la instrucción if salta en algún lugar dentro del siguientes opcodes. De todos modos, es simplemente un juego terminado. Poner un "si" al frente, o incluso envolverlo en una función diferente, no está documentado como "conteniendo" el comportamiento indefinido ... los bits de código no están contaminados con un comportamiento indefinido. El Estándar dice consistentemente que "el programa tiene comportamiento indefinido ".


No. Ejemplo:

struct T { void f() { } }; int main() { T *t = nullptr; if (t) { t->f(); // UB if t == nullptr but since the code tested against that } }


Decidir si un programa realizará una división entera entre 0 (que es UB) es en general equivalente al problema de detención. No hay forma de que un compilador pueda determinar eso, en general. Y así, la mera presencia de UB posible no puede afectar lógicamente al resto del programa: un requisito a tal efecto en la norma, requeriría que cada proveedor de compiladores proporcione un solucionador de problemas en el compilador.

Aún más simple, el siguiente programa tiene UB solo si el usuario ingresa 0:

#include <iostream> using namespace std; auto main() -> int { int x; if( cin >> x ) cout << 100/x << endl; }

Sería absurdo sostener que este programa en sí mismo tiene UB.

Sin embargo, una vez que se produce el comportamiento indefinido, entonces puede ocurrir cualquier cosa: la ejecución adicional del código en el programa se ve comprometida (por ejemplo, la pila podría haberse ensuciado).