Comportamiento indefinido, no especificado y definido por la implementación

530

¿Qué es el comportamiento indefinido en C y C ++? ¿Qué pasa con el comportamiento no especificado y el comportamiento definido por la implementación? ¿Cuál es la diferencia entre ellos?

Zolomon
fuente
1
Estaba bastante seguro de que hemos hecho esto antes, pero no puedo encontrarlo. Ver también: stackoverflow.com/questions/2301372/…
dmckee --- ex-moderador gatito
1
Aquí hay una discusión interesante (la sección "Anexo L y comportamiento indefinido").
Owen

Respuestas:

407

El comportamiento indefinido es uno de esos aspectos del lenguaje C y C ++ que puede sorprender a los programadores que vienen de otros lenguajes (otros lenguajes intentan ocultarlo mejor). Básicamente, es posible escribir programas en C ++ que no se comporten de manera predecible, ¡aunque muchos compiladores de C ++ no informarán ningún error en el programa!

Veamos un ejemplo clásico:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

La variable papunta al literal de cadena "hello!\n", y las dos asignaciones a continuación intentan modificar ese literal de cadena. ¿Qué hace este programa? De acuerdo con la sección 2.14.5, párrafo 11 del estándar C ++, invoca un comportamiento indefinido :

El efecto de intentar modificar un literal de cadena no está definido.

Puedo escuchar a la gente gritar "Pero espera, puedo compilar esto sin problemas y obtener el resultado yellow" o "¿Qué quieres decir con indefinido, los literales de cadena se almacenan en la memoria de solo lectura, por lo que el primer intento de asignación resulta en un volcado del núcleo". Este es exactamente el problema con el comportamiento indefinido. Básicamente, el estándar permite que cualquier cosa suceda una vez que invocas un comportamiento indefinido (incluso demonios nasales). Si hay un comportamiento "correcto" de acuerdo con su modelo mental del lenguaje, ese modelo simplemente está equivocado; El estándar C ++ tiene el único voto, punto.

Otros ejemplos de comportamiento indefinido incluyen acceder a una matriz más allá de sus límites, desreferenciar el puntero nulo , acceder a objetos después de que finalice su vida útil o escribir expresiones supuestamente inteligentes como i++ + ++i.

La Sección 1.9 del estándar C ++ también menciona los dos hermanos menos peligrosos del comportamiento indefinido, el comportamiento no especificado y el comportamiento definido por la implementación :

Las descripciones semánticas en esta Norma Internacional definen una máquina abstracta no determinizada parametrizada.

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.

Ciertos otros aspectos y operaciones de la máquina abstracta se describen en esta Norma Internacional como no especificados (por ejemplo, el orden de evaluación de los argumentos de una función). Siempre que sea posible, esta Norma Internacional define un conjunto de comportamientos permitidos. Estos definen los aspectos no deterministas de la máquina abstracta.

Algunas otras operaciones se describen en esta Norma Internacional como indefinidas (por ejemplo, el efecto de desreferenciar el puntero nulo). [ Nota : esta Norma Internacional no impone requisitos sobre el comportamiento de los programas que contienen un comportamiento indefinido. - nota final ]

Específicamente, la sección 1.3.24 establece:

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).

¿Qué puede hacer para evitar tener un comportamiento indefinido? Básicamente, tienes que leer buenos libros de C ++ de autores que saben de lo que están hablando. Tornillo tutoriales de internet. Tornillo bullschildt.

flujo libre
fuente
66
Es un hecho extraño que resultó de la combinación de que esta respuesta solo cubre C ++, pero las etiquetas de esta pregunta incluyen C. C tiene una noción diferente de "comportamiento indefinido": aún requerirá la implementación para dar mensajes de diagnóstico incluso si el comportamiento también se indica a estar indefinido para ciertas violaciones de reglas (violaciones de restricciones).
Johannes Schaub - litb
8
@Benoit Es un comportamiento indefinido porque el estándar dice que es un comportamiento indefinido, punto. En algunos sistemas, de hecho, los literales de cadena se almacenan en el segmento de texto de solo lectura, y el programa se bloqueará si intenta modificar un literal de cadena. En otros sistemas, el literal de cadena aparecerá de hecho como un cambio. La norma no exige lo que tiene que suceder. Eso es lo que significa un comportamiento indefinido.
fredoverflow
55
@FredOverflow, ¿por qué un buen compilador nos permite compilar código que proporciona un comportamiento indefinido? ¿Exactamente qué bien puede ofrecer compilar este tipo de código? ¿Por qué no todos los buenos compiladores nos dieron una gran señal de advertencia roja cuando intentamos compilar código que proporcione un comportamiento indefinido?
Pacerier
14
@Pacerier Hay ciertas cosas que no son verificables en tiempo de compilación. Por ejemplo, no siempre es posible garantizar que un puntero nulo nunca se desreferencia, pero esto no está definido.
Tim Seguine
44
@Celeritas, el comportamiento indefinido puede ser no determinista. Por ejemplo, es imposible saber de antemano cuáles serán los contenidos de la memoria no inicializada, por ejemplo. int f(){int a; return a;}: el valor de apuede cambiar entre llamadas a funciones.
Mark
97

Bueno, esto es básicamente un simple copiar y pegar del estándar

3.4.1 1 comportamiento definido por la implementación comportamiento no especificado donde cada implementación documenta cómo se hace la elección

2 EJEMPLO Un ejemplo de comportamiento definido por la implementación es la propagación del bit de orden superior cuando un entero con signo se desplaza a la derecha.

3.4.3 1 comportamiento de comportamiento indefinido , al usar una construcción de programa no portable 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, 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).

3 EJEMPLO Un ejemplo de comportamiento indefinido es el comportamiento en el desbordamiento de enteros.

3.4.4 1 comportamiento no especificado uso de un valor no especificado u otro comportamiento donde esta Norma Internacional ofrece dos o más posibilidades y no impone requisitos adicionales sobre los cuales se elige en ningún caso

2 EJEMPLO Un ejemplo de comportamiento no especificado es el orden en que se evalúan los argumentos de una función.

Hormiga
fuente
3
¿Cuál es la diferencia entre el comportamiento definido por la implementación y el no especificado?
Zolomon
26
@Zolomon: Al igual que dice: básicamente lo mismo, excepto que en el caso de la implementación definida, la implementación debe documentar (para garantizar) lo que va a suceder exactamente, mientras que en caso de no especificarse, la implementación no es necesaria para documentar o garantizar cualquier cosa.
An
1
@Zolomon: se refleja en la diferencia entre 3.4.1 y 2.4.4.
sbi
8
@ Celeritas: los compiladores hipermodernos pueden hacerlo mejor que eso. Dado que int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }un compilador puede determinar que, dado que todos los medios para invocar la función que no ejecuta los misiles invocan Comportamiento indefinido, puede hacer que la llamada sea launch_missiles()incondicional.
supercat
2
@northerner Como dice la cita, el comportamiento no especificado generalmente está restringido a un conjunto limitado de posibles comportamientos. En algunos casos, incluso puede llegar a la conclusión de que todas estas posibilidades son aceptables en el contexto dado, en cuyo caso el comportamiento no especificado no es un problema en absoluto. El comportamiento indefinido no tiene restricciones (por ejemplo, "el programa puede decidir formatear su disco duro"). El comportamiento indefinido siempre es un problema.
ANT
60

Tal vez una redacción fácil podría ser más fácil de entender que la definición rigurosa de los estándares.

comportamiento definido por la implementación
El lenguaje dice que tenemos tipos de datos. Los vendedores del compilador especifican qué tamaños usarán y proporcionan una documentación de lo que hicieron.

comportamiento indefinido
Estás haciendo algo mal. Por ejemplo, tiene un valor muy grande en un intque no encaja char. ¿Cómo se pone ese valor char? en realidad no hay manera! Podría pasar cualquier cosa, pero lo más sensato sería tomar el primer byte de ese int y ponerlo char. Es simplemente incorrecto hacer eso para asignar el primer byte, pero eso es lo que sucede debajo del capó.

comportamiento no especificado
¿Qué función de estos dos se ejecuta primero?

void fun(int n, int m);

int fun1()
{
  cout << "fun1";
  return 1;
}
int fun2()
{
  cout << "fun2";
  return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

¡El idioma no especifica la evaluación, de izquierda a derecha o de derecha a izquierda! Por lo tanto, un comportamiento no especificado puede o no resultar en un comportamiento indefinido, pero ciertamente su programa no debe producir un comportamiento no especificado.


@eSKay Creo que su pregunta vale la pena editar la respuesta para aclarar más :)

porque fun(fun1(), fun2());no es el comportamiento "implementación definida"? ¿El compilador tiene que elegir uno u otro curso, después de todo?

La diferencia entre implementación definida y no especificada, es que se supone que el compilador elige un comportamiento en el primer caso, pero no tiene que hacerlo en el segundo caso. Por ejemplo, una implementación debe tener una y solo una definición de sizeof(int). Por lo tanto, no se puede decir que sizeof(int)es 4 para una parte del programa y 8 para otras. A diferencia del comportamiento no especificado, donde el compilador puede decir OK, evaluaré estos argumentos de izquierda a derecha y los argumentos de la siguiente función se evaluarán de derecha a izquierda. Puede suceder en el mismo programa, por eso se llama no especificado . De hecho, C ++ podría haberse hecho más fácil si se especificaran algunos de los comportamientos no especificados. Eche un vistazo aquí a la respuesta del Dr. Stroustrup para eso :

Se afirma que la diferencia entre lo que se puede producir dando al compilador esta libertad y que requiere una "evaluación ordinaria de izquierda a derecha" puede ser significativa. No estoy convencido, pero con innumerables compiladores "allá afuera" aprovechando la libertad y algunas personas defendiendo apasionadamente esa libertad, un cambio sería difícil y podría llevar décadas penetrar en los rincones distantes de los mundos C y C ++. Estoy decepcionado de que no todos los compiladores adviertan contra código como ++ i + i ++. Del mismo modo, el orden de evaluación de los argumentos no está especificado.

En mi opinión, demasiadas "cosas" quedan sin definir, sin especificar, definidas por la implementación, etc. Sin embargo, eso es fácil de decir e incluso dar ejemplos, pero difícil de solucionar. También se debe tener en cuenta que no es tan difícil evitar la mayoría de los problemas y producir código portátil.

AraK
fuente
1
porque fun(fun1(), fun2());no es el comportamiento "implementation defined"? ¿El compilador tiene que elegir uno u otro curso, después de todo?
Lazer
1
@AraK: gracias por la explicación. Lo entiendo ahora. Por cierto, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"entiendo que esto cansuceda. ¿Realmente, con los compiladores que usamos en estos días?
Lazer
1
@eSKay Tienes que preguntarle a un gurú sobre esto que se ensució las manos con muchos compiladores :) AFAIK VC evalúa los argumentos de derecha a izquierda siempre.
AraK
44
@Lazer: Definitivamente puede suceder. Escenario simple: foo (bar, boz ()) y foo (boz (), bar), donde bar es un int y boz () es una función que devuelve int. Suponga una CPU donde se espera que los parámetros se pasen en los registros R0-R1. Los resultados de la función se devuelven en R0; las funciones pueden destruir R1. Evaluar "bar" antes de "boz ()" requeriría guardar una copia de la barra en otro lugar antes de llamar a boz () y luego cargar esa copia guardada. Evaluar "bar" después de "boz ()" evitará un almacenamiento de memoria y volver a buscar, y es una optimización que muchos compiladores harían independientemente de su orden en la lista de argumentos.
supercat
66
No sé acerca de C ++, pero el estándar C dice que una conversión de un int a un char está definida por la implementación o incluso bien definida (dependiendo de los valores reales y la firma de los tipos). Ver C99 §6.3.1.3 (sin cambios en C11).
Nikolai Ruhe
27

Del documento oficial de Justificación C

Los términos comportamiento no especificado , comportamiento indefinido y definición de implementación comportamiento se utilizan para clasificar el resultado de escribir programas cuyas propiedades el Estándar no describe o no puede describir por completo. El objetivo de adoptar esta categorización es permitir una cierta variedad de implementaciones que permita que la calidad de implementación sea una fuerza activa en el mercado, así como permitir ciertas extensiones populares, sin eliminar el prestigio de conformidad con el Estándar. El Apéndice F de la Norma cataloga aquellos comportamientos que se incluyen en una de estas tres categorías.

El comportamiento no especificado le da al implementador cierta libertad para traducir programas. Esta latitud no se extiende hasta no poder traducir el programa.

El comportamiento indefinido le otorga al implementador la licencia para no detectar ciertos errores del programa que son difíciles de diagnosticar. También identifica áreas de posible extensión de lenguaje conforme: el implementador puede aumentar el lenguaje al proporcionar una definición del comportamiento oficialmente indefinido.

El comportamiento definido por la implementación le da al implementador la libertad de elegir el enfoque apropiado, pero requiere que se explique esta opción al usuario. Los comportamientos designados como definidos por la implementación son generalmente aquellos en los que un usuario podría tomar decisiones de codificación significativas basadas en la definición de implementación. Los implementadores deben tener en cuenta este criterio al decidir qué tan extensa debe ser una definición de implementación. Al igual que con el comportamiento no especificado, simplemente no traducir la fuente que contiene el comportamiento definido por la implementación no es una respuesta adecuada.

Johannes Schaub - litb
fuente
3
Los escritores de compiladores hipermodernos también consideran que el "comportamiento indefinido" otorga a los escritores de compiladores la licencia para asumir que los programas nunca recibirán entradas que puedan causar un Comportamiento indefinido y cambiar arbitrariamente todos los aspectos de cómo se comportan los programas cuando reciben tales entradas.
supercat
2
Otro punto que acabo de notar: C89 no usó el término "extensión" para describir características que estaban garantizadas en algunas implementaciones pero no en otras. Los autores de C89 reconocieron que la mayoría de las implementaciones actuales tratarían la aritmética con signo y la aritmética sin signo de manera idéntica, excepto cuando los resultados se usaran de ciertas maneras, y dicho tratamiento se aplicaría incluso en caso de desbordamiento firmado; sin embargo, no mencionaron eso como una extensión común en el Anexo J2, lo que me sugiere que lo vieron como un estado de cosas natural, en lugar de una extensión.
supercat
10

El comportamiento indefinido frente al comportamiento no especificado tiene una breve descripción.

Su resumen final:

En resumen, el comportamiento no especificado suele ser algo de lo que no debe preocuparse, a menos que se requiera que su software sea portátil. Por el contrario, el comportamiento indefinido siempre es indeseable y nunca debe ocurrir.

Anders Abel
fuente
1
Hay dos tipos de compiladores: aquellos que, a menos que se documente explícitamente lo contrario, interpretan la mayoría de las formas de Comportamiento indefinido del Estándar como recurriendo a comportamientos característicos documentados por el entorno subyacente, y aquellos que por defecto solo exponen comportamientos útiles que el Estándar caracteriza como Implementado-Definido. Cuando se usan compiladores del primer tipo, muchas cosas del primer tipo se pueden hacer de manera eficiente y segura usando UB. Los compiladores para el segundo tipo solo serán adecuados para tales tareas si ofrecen opciones para garantizar el comportamiento en tales casos.
supercat
8

Históricamente, tanto el Comportamiento definido por la implementación como el Comportamiento indefinido representaban situaciones en las que los autores de la Norma esperaban que las personas que escriben implementaciones de calidad usarían el juicio para decidir qué garantías de comportamiento, si las hubiera, serían útiles para los programas en el campo de aplicación previsto que se ejecuta en el objetivos previstos. Las necesidades del código de procesamiento de números de gama alta son bastante diferentes de las del código de sistemas de bajo nivel, y tanto UB como IDB brindan flexibilidad a los escritores de compiladores para satisfacer esas diferentes necesidades. Ninguna de las categorías exige que las implementaciones se comporten de una manera que sea útil para un propósito en particular, o incluso para cualquier propósito. Sin embargo, las implementaciones de calidad que afirman ser adecuadas para un propósito particular, deben comportarse de una manera apropiada para dicho propósito.si la Norma lo requiere o no .

La única diferencia entre el comportamiento definido por la implementación y el comportamiento indefinido es que el primero requiere que las implementaciones definan y documenten un comportamiento coherente incluso en los casos en que nada de lo que la implementación podría hacer sería útil . La línea divisoria entre ellos no es si en general sería útil para las implementaciones definir comportamientos (los escritores de compiladores deberían definir comportamientos útiles cuando sea práctico si el Estándar lo requiere o no) sino si podría haber implementaciones donde definir un comportamiento sería simultáneamente costoso e inútil . El juicio de que tales implementaciones podrían existir no implica, de ninguna manera o forma, ningún juicio sobre la utilidad de soportar un comportamiento definido en otras plataformas.

Desafortunadamente, desde mediados de la década de 1990, los escritores de compiladores han comenzado a interpretar la falta de mandatos de comportamiento como un juicio de que las garantías de comportamiento no valen la pena, incluso en los campos de aplicación donde son vitales, e incluso en sistemas donde prácticamente no cuestan nada. En lugar de tratar a UB como una invitación a ejercer un juicio razonable, los escritores de compiladores han comenzado a tratarlo como una excusa para no hacerlo.

Por ejemplo, dado el siguiente código:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

una implementación de complemento a dos no tendría que hacer ningún esfuerzo para tratar la expresión v << powcomo un cambio de complemento a dos sin tener en cuenta si vfue positiva o negativa.

La filosofía preferida entre algunos de los escritores de compiladores de hoy, sin embargo, sugeriría que debido a que vsolo puede ser negativo si el programa va a participar en Comportamiento indefinido, no hay razón para que el programa recorte el rango negativo v. Aunque el desplazamiento hacia la izquierda de los valores negativos solía ser compatible con cada compilador significativo, y una gran cantidad de código existente depende de ese comportamiento, la filosofía moderna interpretaría el hecho de que el Estándar dice que los valores negativos hacia la izquierda son UB como implicando que los escritores del compilador deberían sentirse libres de ignorar eso.

Super gato
fuente
Pero manejar el comportamiento indefinido de una manera agradable no es gratis. Toda la razón por la cual los compiladores modernos exhiben un comportamiento tan extraño en algunos casos de UB es porque están optimizando sin descanso, y para hacer el mejor trabajo en eso, tienen que ser capaces de asumir que UB nunca ocurre.
Tom Swirly
¡Pero el hecho de que <<UB tenga números negativos es una pequeña trampa desagradable y me alegra que me lo recuerden!
Tom Swirly
1
@TomSwirly: Desafortunadamente, a los escritores de compiladores no les importa que ofrecer garantías de comportamiento poco estrictas más allá de las exigidas por el Estándar a menudo puede permitir un aumento de velocidad masivo en comparación con exigir que el código evite a toda costa cualquier cosa no definida por el Estándar. Si a un programador no le importa si i+j>kproduce 1 o 0 en los casos en que la adición se desborda, siempre que no tenga otros efectos secundarios , un compilador puede realizar algunas optimizaciones masivas que no serían posibles si el programador escribiera el código como (int)((unsigned)i+j) > k.
supercat
1
@TomSwirly: Para ellos, si el compilador X puede tomar un programa estrictamente conforme para realizar alguna tarea T y generar un ejecutable que sea un 5% más eficiente que el compilador Y con ese mismo programa, eso significa que X es mejor, incluso si Y podría generar código que hiciera la misma tarea tres veces más eficientemente dado un programa que explota comportamientos que Y garantiza pero X no.
supercat
6

C ++ estándar n3337 § 1.3.10 comportamiento definido por la implementación

comportamiento, para una construcción de programa bien formada y datos correctos, que depende de la implementación y que cada implementación documenta

A veces, C ++ Standard no impone un comportamiento particular en algunas construcciones, sino que dice que un comportamiento particular y bien definido debe ser elegido y descrito por una implementación particular (versión de la biblioteca). Por lo tanto, el usuario aún puede saber exactamente cómo se comportará el programa, aunque Standard no lo describa.


C ++ estándar n3337 § 1.3.24 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 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. - nota final]

Cuando el programa encuentra una construcción que no está definida de acuerdo con el Estándar C ++, se le permite hacer lo que quiera hacer (tal vez enviarme un correo electrónico o quizás enviarle un correo electrónico o ignorar el código por completo).


C ++ estándar n3337 § 1.3.25 comportamiento no especificado

comportamiento, para una construcción de programa bien formada y datos correctos, que depende de la implementación [Nota: La implementación no es necesaria para documentar qué comportamiento ocurre. El rango de posibles comportamientos generalmente está delineado por esta Norma Internacional. - nota final]

C ++ Standard no impone un comportamiento particular en algunas construcciones, sino que dice que se debe elegir un comportamiento particular y bien definido ( no se describe necesariamente el bot ) mediante una implementación particular (versión de la biblioteca). Entonces, en el caso de que no se haya proporcionado una descripción, puede ser difícil para el usuario saber exactamente cómo se comportará el programa.

4pie0
fuente
6

Implementación definida

Los implementadores desean, deben estar bien documentados, el estándar ofrece opciones, pero seguro que compilará

Sin especificar

Igual que la implementación definida pero no documentada

Indefinido

Cualquier cosa puede pasar, cuídalo.

Suraj K Thomas
fuente
2
Creo que es importante tener en cuenta que el significado práctico de "indefinido" ha cambiado en los últimos años. Solía ​​ser eso dado uint32_t s;, se podría esperar que evaluar 1u<<scuándo ses 33 pueda producir 0 o tal vez 2, pero no hacer nada más raro. Sin embargo, los compiladores más nuevos 1u<<spueden hacer que un compilador determine que debido a que sdebe haber sido inferior a 32 de antemano, sse puede omitir cualquier código anterior o posterior a esa expresión que solo sería relevante si hubiera sido 32 o superior.
supercat