¿Puede el código que nunca se ejecutará invocar un comportamiento indefinido?

81

El código que invoca un comportamiento indefinido (en este ejemplo, división por cero) nunca se ejecutará, ¿el programa todavía tiene un comportamiento indefinido?

int main(void)
{
    int i;
    if(0)
    {
        i = 1/0;
    }
    return 0;
}

Creo que todavía es un comportamiento indefinido, pero no puedo encontrar ninguna evidencia en el estándar que me apoye o niegue.

Entonces, ¿alguna idea?

Yu Hao
fuente
7
Yo diría que no es "comportamiento" si nunca se ejecuta
Kevin
1
Si UB es el tiempo de ejecución uno (como este), no lo haría. Pero dudo mucho que Standard diga algo sobre esto.
Keltar
13
Suena como una cuestión de semántica, no de programación.
Wooble
14
@Wooble No estoy de acuerdo. La frase comportamiento indefinido tiene un significado especial en C / C ++. Y esta pregunta está relacionada con algunas otras situaciones que determinan un comportamiento indefinido o no. Para el registro, si ha leído el estándar C / C ++, encontrará la frase comportamiento indefinido en todas partes.
Yu Hao
10
@Cornstalks: El estándar C no usa la frase "invoca un comportamiento indefinido", por lo que no puede razonar sobre el estándar C basándose en lo que podría significar esta frase. Usarlo para describir C es inapropiado porque sugiere que el “comportamiento indefinido” es algo como una pared con la que te encuentras si te salgas de los límites. En realidad, "comportamiento indefinido" es una falta de algo; es el fin de las fronteras. Cuando abandona la ciudad bien definida que es el estándar C, se encuentra en un campo abierto donde se puede construir cualquier cosa.
Eric Postpischil

Respuestas:

70

Veamos cómo el estándar C define los términos "comportamiento" y "comportamiento indefinido".

Las referencias son al borrador N1570 de la norma ISO C 2011; No tengo conocimiento de ninguna diferencia relevante en ninguna de las tres normas ISO C publicadas (1990, 1999 y 2011).

Sección 3.4:

comportamiento
apariencia o acción externa

Ok, eso es un poco vago, pero yo diría que una declaración dada no tiene "apariencia", y ciertamente no tiene "acción", a menos que realmente se ejecute.

Sección 3.4.3:

Comportamiento indefinido comportamiento
, ante el uso de una construcción de programa no portátil o errónea o de datos erróneos, para los cuales esta Norma Internacional no impone requisitos.

Dice " sobre el uso " de tal construcción. La palabra "uso" no está definida por el estándar, por lo que recurrimos al significado común en inglés. Una construcción no se "usa" si nunca se ejecuta.

Hay una nota bajo esa definición:

NOTA El posible comportamiento indefinido va desde ignorar la situación por completo con resultados impredecibles, hasta comportarse durante la traducción o 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).

Por lo tanto, un compilador puede rechazar su programa en tiempo de compilación si su comportamiento no está definido. Pero mi interpretación de eso es que puede hacerlo solo si puede probar que cada ejecución del programa encontrará un comportamiento indefinido. Lo que implica, creo, que esto:

if (rand() % 2 == 0) {
    i = i / 0;
}

que ciertamente puede tener un comportamiento indefinido, no se puede rechazar en tiempo de compilación.

En la práctica, los programas deben poder realizar pruebas en tiempo de ejecución para evitar invocar un comportamiento indefinido, y el estándar debe permitirles hacerlo.

Tu ejemplo fue:

if (0) {
    i = 1/0;
}

que nunca ejecuta la división entre 0. Un modismo muy común es:

int x, y;
/* set values for x and y */
if (y != 0) {
    x = x / y;
}

La división ciertamente tiene un comportamiento indefinido si y == 0, pero nunca se ejecuta si y == 0. El comportamiento está bien definido y por la misma razón que su ejemplo está bien definido: porque el comportamiento potencial indefinido nunca puede suceder realmente.

(A menos que INT_MIN < -INT_MAX && x == INT_MIN && y == -1(sí, la división de enteros puede desbordarse), pero ese es un problema aparte).

En un comentario (ya eliminado), alguien señaló que el compilador puede evaluar expresiones constantes en tiempo de compilación. Lo cual es cierto, pero no relevante en este caso, porque en el contexto de

i = 1/0;

1/0 no es una expresión constante .

Una expresión-constante es una categoría sintáctica que se reduce a expresión-condicional (que excluye asignaciones y expresiones de coma). La expresión-constante de producción aparece en la gramática solo en contextos que realmente requieren una expresión constante, como las etiquetas de caso. Entonces, si escribe:

switch (...) {
    case 1/0:
    ...
}

entonces 1/0es una expresión constante - y una que viola la restricción en 6.6p4: "Cada expresión constante se evaluará a una constante que está en el rango de valores representables para su tipo", por lo que se requiere un diagnóstico. Pero el lado derecho de una asignación no requiere una expresión-constante , simplemente una expresión-condicional , por lo que las restricciones en las expresiones constantes no se aplican. Un compilador puede evaluar cualquier expresión que pueda en tiempo de compilación, pero solo si el comportamiento es el mismo que si fuera evaluado durante la ejecución (o, en el contexto de if (0), no evaluado durante la ejecución ().

(Algo que se ve exactamente como una expresión-constante no es necesariamente una expresión-constante , así como, en x + y * z, la secuencia x + yno es una expresión-aditiva debido al contexto en el que aparece).

Lo que significa la nota al pie de la sección 6.6 de N1570 que iba a citar:

Por tanto, en la siguiente inicialización,
static int i = 2 || 1 / 0;
la expresión es una expresión constante entera válida con valor uno.

no es realmente relevante para esta pregunta.

Finalmente, hay algunas cosas que están definidas para causar un comportamiento indefinido que no se trata de lo que sucede durante la ejecución. El anexo J, sección 2 de la norma C (nuevamente, consulte el borrador N1570 ) enumera las cosas que causan un comportamiento indefinido, recopiladas del resto de la norma. Algunos ejemplos (no afirmo que esta sea una lista exhaustiva) son:

  • Un archivo de origen no vacío no termina en un carácter de nueva línea que no está inmediatamente precedido por un carácter de barra invertida o termina en un token o comentario de preprocesamiento parcial
  • La concatenación de tokens produce una secuencia de caracteres que coincide con la sintaxis de un nombre de carácter universal
  • Un carácter que no está en el juego de caracteres de origen básico se encuentra en un archivo de origen, excepto en un identificador, una constante de carácter, un literal de cadena, un nombre de encabezado, un comentario o un token de preprocesamiento que nunca se convierte en un token
  • Un identificador, comentario, literal de cadena, constante de caracteres o nombre de encabezado contiene un carácter multibyte no válido o no comienza ni termina en el estado de cambio inicial
  • El mismo identificador tiene vínculos internos y externos en la misma unidad de traducción

Estos casos particulares son cosas que un compilador podría detectar. Creo que su comportamiento no está definido porque el comité no quería, o no podía, imponer el mismo comportamiento en todas las implementaciones, y definir un rango de comportamientos permitidos simplemente no valía la pena. Realmente no entran en la categoría de "código que nunca se ejecutará", pero los menciono aquí para completarlos.

Keith Thompson
fuente
2
@EricPostpischil 6.6 / 4 dice "Cada expresión constante se evaluará a una constante que está en el rango de valores representables para su tipo". ¿No excluiría eso 1/0de ser una expresión constante?
Casey
2
@EricPostpischil: No creo que sea del todo correcto. Violar una restricción generalmente significa que se requiere un diagnóstico en tiempo de compilación, no simplemente que algo que de otro modo podría ser un foo no es un foo . 1/0no es una expresión constante en el contexto de la pregunta porque no se analiza como una expresión constante , simplemente como una expresión condicional que forma parte de una expresión de asignación . case 1/0:violaría la restricción y requeriría un diagnóstico.
Keith Thompson
DR # 109 parece indicar que el programa no es UB, vea la nueva respuesta que acabo de publicar.
ouah
1
Re " para volver al significado común en inglés ", en el significado en inglés, el programa usa una construcción si está presente en el programa. Entonces, ¿por qué su respuesta asume que usar una construcción significa ejecutar una construcción? ¡Tu conclusión no se sigue de tu explicación!
ikegami
31

Este artículo analiza esta cuestión en la sección 2.6:

int main(void){
      guard();
      5 / 0;
}

Los autores consideran que el programa se define cuando guard()no termina. También se encuentran distinguiendo las nociones de "estáticamente indefinido" y "dinámicamente indefinido", por ejemplo:

La intención detrás del estándar 11 parece ser que, en general, las situaciones se vuelven estáticamente indefinidas si no es fácil generar código para ellas. Solo cuando se puede generar código, la situación puede ser indefinida dinámicamente.

11) Correspondencia privada con miembro del comité.

Recomendaría mirar el artículo completo. En conjunto, pinta una imagen coherente.

El hecho de que los autores del artículo tuvieran que discutir la pregunta con un miembro del comité confirma que el estándar actualmente es confuso en la respuesta a su pregunta.

Pascal Cuoq
fuente
1
Ese ejemplo presenta dificultades solo en cuanto a si puede determinar estáticamente si su comportamiento no está definido. Cuando se ejecuta (asumiendo que el comportamiento de guard()no es indefinido), el comportamiento es indefinido si y solo si la declaración 5 / 0;se ejecuta realmente. (Tenga en cuenta que un compilador podría reemplazar legítimamente la evaluación de 5 / 0con una llamada a abort()o algo similar; el programa entonces abortaría si y solo si la ejecución llega a ese punto). Un compilador puede rechazar ese programa solo si puede determinar que guard()siempre terminará.
Keith Thompson
@KeithThompson Para aclarar la distinción estática / dinámica en el artículo, 5/0 se considera dinámico porque el compilador puede generar código que divide por cero: simplemente genera el código habitual que divide por z después de haber establecido z en 0. Por lo tanto, un compilador ingenuo puede generar una instrucción de división. Un compilador sofisticado que determina que guard()no termina no tiene que generar ningún código para 5/0. Por el contrario, no hay forma de generar código para (int)(void)5, no se puede simplemente generar el código para (int)(void)z, ya que eso tampoco es correcto. Entonces los autores piensan que ...
Pascal Cuoq
@KeithThompson ... un compilador puede rechazar el programa if (0) (int)(void)5;debido al enigma que presenta al compilador ingenuo, mientras que el UB dinámico inalcanzable como if (0) 5 / 0;es inofensivo. Esto es lo que trascendió de su discusión con un miembro del comité y he visto un argumento similar en otro lugar (pero quizás de la misma fuente, especialmente porque no recuerdo dónde estaba). Estoy revisando el fundamento del C99 en este momento, si veo alguna mención de esto, volveré y se lo señalaré.
Pascal Cuoq
2
(int)(void)5es una violación de la restricción. N1570 6.5.4, que describe el operador de conversión: "Restricciones: a menos que el nombre del tipo especifique un tipo vacío, el nombre del tipo debe especificar el tipo escalar atómico, calificado o no calificado, y el operando debe tener un tipo escalar". (void)5no tiene tipo escalar, por lo (int)(void)5que viola esa restricción, independientemente de si el código que lo contiene se ejecuta alguna vez.
Keith Thompson
@KeithThompson Sí, parecen haber elegido el ejemplo equivocado, pero dentro de la larga lista en J.2, hay uno que no es una violación de restricción y que es “estático”, ¿no? ¿Qué tal ese viejo clásico, "Un archivo fuente no vacío no termina en un carácter de nueva línea ..."? No existe una noción de accesibilidad que se aplique a este, pero no es una violación de restricción, ¿verdad?
Pascal Cuoq
5

En este caso, el comportamiento indefinido es el resultado de ejecutar el código. Entonces, si el código no se ejecuta, no hay un comportamiento indefinido.

El código no ejecutado podría invocar un comportamiento indefinido si el comportamiento indefinido fuera el resultado únicamente de la declaración del código (por ejemplo, si algún caso de sombreado de variables no estaba definido).

Arnaud Le Blanc
fuente
Para ver un ejemplo del caso n. ° 2, considere #include "//e"cuál invoca a UB.
Michael Foukarakis
2

Yo iría con el último párrafo de esta respuesta: https://stackoverflow.com/a/18384176/694576

... UB es un problema de tiempo de ejecución, no un problema de tiempo de compilación ...

Entonces, no, no se invoca ningún UB.

alk
fuente
2
No debe creer todo lo que lee en Internet, especialmente no de esa respuesta de StackOverflow.
Pascal Cuoq
1
@PascalCuoq Eso rompe la fe de varios creyentes de SO como yo. ¿Adónde ir ahora?
0decimal0
2

Solo cuando el estándar realiza cambios importantes y su código de repente ya no "nunca se ejecuta". Pero no veo ninguna forma lógica en la que esto pueda causar un 'comportamiento indefinido'. No está causando nada .

Hrishi
fuente
2

En el tema del comportamiento indefinido, a menudo es difícil separar los aspectos formales de los prácticos. Esta es la definición de comportamiento indefinido en el estándar de 1989 (no tengo una versión más reciente a la mano, pero no espero que esto haya cambiado sustancialmente):

1 comportamiento indefinido
  comportamiento, sobre el uso de una construcción de programa no portátil o errónea o de
  datos erróneos, para los cuales esta norma internacional no impone requisitos
2 NOTA El posible comportamiento indefinido varía desde ignorar la situación por completo
  con resultados impredecibles, a comportarse durante la traducción o ejecución del programa
  de manera documentada característica del medio ambiente (con o sin el
  emisión de un mensaje de diagnóstico), a la finalización de una traducción o
  ejecución (con la emisión de un mensaje de diagnóstico).

Desde un punto de vista formal, diría que su programa invoca un comportamiento indefinido, lo que significa que el estándar no impone ningún requisito sobre lo que hará cuando se ejecute, solo porque contiene división por cero.

Por otro lado, desde un punto de vista práctico, me sorprendería encontrar un compilador que no se comportara como esperas intuitivamente.

Nicola Musatti
fuente
2

El estándar dice, como recuerdo bien, está permitido hacer cualquier cosa desde el momento en que se rompió una regla. Tal vez haya algunos eventos especiales con una especie de sabor global (pero nunca escuché o leí sobre algo así) ... Entonces yo diría: No, esto no puede ser UB, porque mientras el comportamiento esté bien definido, 0 es siempre falso, por lo que la regla no se puede romper en tiempo de ejecución.

dhein
fuente
0 es siempre cierto? o incluso siempre cierto? ¡¿Eres una especie de rubí ?!
Grady Player
@Grady PlayerNo, yo era sólo una especie de Brain afk. Lo voy a arreglar, lo siento
dhein
2

Creo que todavía es un comportamiento indefinido, pero no puedo encontrar ninguna evidencia en el estándar que me apoye o niegue.

Creo que el programa no invoca un comportamiento indefinido.

El Informe de defectos n. ° 109 aborda una pregunta similar y dice:

Además, si cada posible ejecución de un programa dado resultara en un comportamiento indefinido, el programa dado no es estrictamente conforme. Una implementación conforme no debe dejar de traducir un programa estrictamente conforme simplemente porque alguna posible ejecución de ese programa daría como resultado un comportamiento indefinido. Dado que es posible que nunca se llame a foo, el ejemplo dado debe traducirse con éxito mediante una implementación conforme.

ouah
fuente
-1

Depende de cómo se defina la expresión "comportamiento indefinido" y de si "comportamiento indefinido" de una declaración es lo mismo que "comportamiento indefinido" para un programa.

Este programa se parece a C, por lo que es apropiado un análisis más profundo de cuál es el estándar C utilizado por el compilador (como lo hicieron algunas respuestas).

En ausencia de un estándar específico, la respuesta correcta es "depende". En algunos lenguajes, los compiladores después del primer error intentan adivinar lo que podría significar el programador y aún así generan algo de código, según la conjetura de los compiladores. En otros lenguajes más puros, una vez que algo está indefinido, la indefinición se propaga a todo el programa.

Otros lenguajes tienen un concepto de "errores limitados". Para algunos tipos limitados de errores, estos lenguajes definen cuánto daño puede producir un error. En particular, los lenguajes con recolección de basura implícita con frecuencia marcan la diferencia si un error invalida el sistema de mecanografía o no.

user3138160
fuente