¿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?
¿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?
Respuestas:
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 p
apunta 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.
int f(){int a; return a;}
: el valor de a
puede cambiar entre llamadas a funciones.
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.
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.
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 int
que 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.
fun(fun1(), fun2());
no es el comportamiento "implementation defined"
? ¿El compilador tiene que elegir uno u otro curso, después de todo?
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
entiendo que esto can
suceda. ¿Realmente, con los compiladores que usamos en estos días?
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.
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.
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 << pow
como un cambio de complemento a dos sin tener en cuenta si v
fue positiva o negativa.
La filosofía preferida entre algunos de los escritores de compiladores de hoy, sin embargo, sugeriría que debido a que v
solo 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.
<<
UB tenga números negativos es una pequeña trampa desagradable y me alegra que me lo recuerden!
i+j>k
produce 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
.
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.
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.
uint32_t s;
, se podría esperar que evaluar 1u<<s
cuándo s
es 33 pueda producir 0 o tal vez 2, pero no hacer nada más raro. Sin embargo, los compiladores más nuevos 1u<<s
pueden hacer que un compilador determine que debido a que s
debe haber sido inferior a 32 de antemano, s
se puede omitir cualquier código anterior o posterior a esa expresión que solo sería relevante si hubiera sido 32 o superior.