studio reales proyectos programacion libro introducción incluye herramientas fundamentos fuente español código con avanzado aplicaciones c++ undefined-behavior

c++ - reales - ¿El comportamiento indefinido solo es un problema si está implementando en varias plataformas?



libro de android studio en español pdf (12)

Nada está cambiando excepto el código, y la UB no está definida por la implementación.

Cambiar el código es suficiente para desencadenar un comportamiento diferente del optimizador con respecto al comportamiento indefinido y, por lo tanto, el código que puede haber funcionado puede romperse fácilmente debido a cambios aparentemente menores que exponen más oportunidades de optimización. Por ejemplo, un cambio que permite que una función esté en línea, esto se cubre bien en Lo que todo programador de C debe saber sobre el comportamiento indefinido # 2/3 que dice:

Si bien esto es intencionalmente un ejemplo simple y artificial, este tipo de cosas suceden todo el tiempo con la alineación: alinear una función a menudo expone una serie de oportunidades de optimización secundaria. Esto significa que si el optimizador decide incorporar una función en línea, una gran variedad de optimizaciones locales pueden entrar en acción, lo que cambia el comportamiento del código. Esto es perfectamente válido según el estándar e importante para el rendimiento en la práctica.

Los proveedores de compiladores se han vuelto muy agresivos con optimizaciones en torno a comportamientos indefinidos y las actualizaciones pueden exponer código previamente no explotado:

Lo importante y aterrador es darse cuenta de que casi cualquier optimización basada en un comportamiento indefinido puede comenzar a activarse en un código defectuoso en cualquier momento en el futuro. La alineación, el desenrollado de bucles, la promoción de memoria y otras optimizaciones seguirán mejorando, y una parte importante de su razón para existir es exponer optimizaciones secundarias como las anteriores.

La mayoría de las conversaciones sobre comportamiento indefinido (UB) hablan sobre cómo hay algunas plataformas que pueden hacer esto, o algunos compiladores lo hacen.

¿Qué sucede si solo está interesado en una plataforma y solo un compilador (misma versión) y sabe que los usará durante años?

Nada está cambiando excepto el código, y la UB no está definida por la implementación.

Una vez que el UB se haya manifestado para esa arquitectura y ese compilador y lo haya probado, ¿no puede suponer que a partir de entonces lo que el compilador hizo con el UB la primera vez, lo hará cada vez?

Nota: Sé que el comportamiento indefinido es muy, muy malo , pero cuando señalé a UB en el código escrito por alguien en esta situación, me preguntaron esto, y no tenía nada mejor que decir, si alguna vez tiene que actualizar o puerto, todo el UB será muy costoso de arreglar.

Parece que hay diferentes categorías de comportamiento:

  1. Defined : este es un comportamiento documentado para funcionar según los estándares
  2. Supported : este es un comportamiento documentado para ser compatible, también conocido como implementación definida
  3. Extensions : esta es una adición documentada, el soporte para operaciones de bits de bajo nivel como popcount , sugerencias de sucursal, se popcount en esta categoría
  4. Constant : si bien no está documentado, estos son comportamientos que probablemente serán consistentes en una plataforma determinada, como la resistencia, el sizeof int aunque no es portátil, es probable que no cambien
  5. Reasonable : generalmente seguro y generalmente heredado, pasando de no firmado a firmado, utilizando el puntero bajo como espacio temporal
  6. Dangerous : leer memoria no inicializada o no asignada, devolver una variable temporal, usar memcopy en una clase no pod

Parece que Constant podría ser invariable dentro de una versión de parche en una plataforma. La línea entre Reasonable y Dangerous parece estar moviendo cada vez más el comportamiento hacia Dangerous medida que los compiladores se vuelven más agresivos en sus optimizaciones.


"El software que no cambia, no se está utilizando".

Si está haciendo algo inusual con los punteros, probablemente haya una forma de usar los moldes para definir lo que desea. Debido a su naturaleza, no serán "lo que el compilador hizo con la UB la primera vez". Por ejemplo, cuando se refiere a la memoria apuntada por un puntero sin inicializar, obtiene una dirección aleatoria que es diferente cada vez que ejecuta el programa.

El comportamiento indefinido generalmente significa que está haciendo algo complicado, y que sería mejor hacer la tarea de otra manera. Por ejemplo, esto no está definido:

printf("%d %d", ++i, ++i);

Es difícil saber cuál sería la intención aquí, y debería reconsiderarse.


Cambiar el código sin romperlo requiere leer y comprender el código actual. Confiar en un comportamiento indefinido perjudica la legibilidad: si no puedo buscarlo, ¿cómo se supone que debo saber qué hace el código?

Si bien la portabilidad del programa podría no ser un problema, la portabilidad de los programadores podría serlo. Si necesita contratar a alguien para mantener el programa, querrá poder buscar simplemente un desarrollador de '' <idioma x> con experiencia en <dominio de aplicación> '' que se adapte bien a su equipo en lugar de tener que encontrar un desarrollador capaz '' Desarrollador <idioma x> con experiencia en <dominio de aplicación> conociendo (o dispuesto a aprender) todos los intrínsecos de comportamiento indefinidos de la versión xyz en la plataforma foo cuando se usa en combinación con bar mientras tiene baz en el furbleblawup '' .


A lo que se refiere es a una implementación más probable que sea un comportamiento definido y no indefinido . La primera es cuando el estándar no le dice lo que sucederá, pero debería funcionar igual si está utilizando el mismo compilador y la misma plataforma. Un ejemplo de esto es asumir que un int tiene 4 bytes de longitud. UB es algo más serio. Allí el estándar no dice nada. Es posible que para un compilador y plataforma determinados funcione, pero también es posible que solo funcione en algunos casos.

Un ejemplo es el uso de valores no inicializados. Si usa un bool no inicializado en un if , puede obtener verdadero o falso, y puede suceder que siempre sea lo que desea, pero el código se romperá de varias maneras sorprendentes.

Otro ejemplo es desreferenciar un puntero nulo. Si bien probablemente dará como resultado una falla predeterminada en todos los casos, pero el estándar no requiere que el programa produzca los mismos resultados cada vez que se ejecuta un programa.

En resumen, si está haciendo algo que está definido en la implementación , entonces está seguro si solo está desarrollando en una plataforma y probó que funciona. Si está haciendo algo que es un comportamiento indefinido , entonces probablemente no esté seguro en ningún caso. Puede ser que funcione, pero nada lo garantiza.


El comportamiento indefinido puede ser alterado por cosas como la temperatura ambiente, lo que hace que cambien las latencias giratorias del disco duro, lo que hace que cambie la programación de los hilos, lo que a su vez cambia el contenido de la basura aleatoria que se está evaluando.

En resumen, no es seguro a menos que el compilador o el sistema operativo especifique el comportamiento (ya que el estándar de idioma no lo hizo).


Esta es básicamente una pregunta sobre una implementación específica de C ++. "¿Puedo suponer que un comportamiento específico, indefinido por el estándar, seguirá siendo manejado por ($ CXX) en la plataforma XYZ de la misma manera en circunstancias UVW?"

Creo que debería aclararlo diciendo exactamente con qué compilador y plataforma está trabajando, y luego consultar su documentación para ver si ofrecen alguna garantía, de lo contrario la pregunta es fundamentalmente sin respuesta.

El punto principal del comportamiento indefinido es que el estándar C ++ no especifica lo que sucede, por lo que si está buscando algún tipo de garantía del estándar de que está "bien", no lo encontrará. Si se pregunta si la "comunidad en general" lo considera seguro, eso se basa principalmente en la opinión.

Una vez que el UB se haya manifestado para esa arquitectura y ese compilador y lo haya probado, ¿no puede suponer que a partir de entonces lo que el compilador hizo con el UB la primera vez, lo hará cada vez?

Solo si los creadores del compilador garantizan que puedes hacer esto, de lo contrario, no, es una ilusión.

Permítanme intentar responder de nuevo de una manera ligeramente diferente.

Como todos sabemos, en la ingeniería de software normal y la ingeniería en general, a los programadores / ingenieros se les enseña a hacer las cosas de acuerdo con un estándar, los escritores de compiladores / fabricantes de piezas producen piezas / herramientas que cumplen con un estándar, y al final se produce algo donde "bajo los supuestos de los estándares, mi trabajo de ingeniería muestra que este producto funcionará", y luego lo prueba y lo envía.

Supongamos que tienes un tío loco jimbo y un día, él sacó todas sus herramientas y un montón de dos por cuatro, y trabajó durante semanas e hizo una montaña rusa improvisada en su patio trasero. Y luego lo ejecutas, y efectivamente no se bloquea. E incluso lo ejecutas diez veces, y no se bloquea. Ahora, Jimbo no es ingeniero, por lo que esto no está hecho de acuerdo con los estándares. Pero si no se bloqueó después de incluso diez veces, eso significa que es seguro y puede comenzar a cobrar la entrada al público, ¿verdad?

En gran medida, lo que es seguro y lo que no es una pregunta sociológica. Pero si solo quiere que sea una simple cuestión de "cuándo puedo asumir razonablemente que nadie se lastimará si cobro la admisión, cuando realmente no puedo asumir nada sobre el producto", así es como lo haría. Suponga que calculo que, si empiezo a cobrar la entrada al público, lo ejecutaré durante X años, y en ese tiempo, tal vez 100,000 personas lo utilizarán. Si se trata básicamente de un lanzamiento de moneda sesgado, ya sea que se rompa o no, entonces lo que me gustaría ver es algo como "este dispositivo se ha ejecutado un millón de veces con muñecos de choque, y nunca se ha estrellado o mostró indicios de rotura". Entonces, podría razonablemente creer que si empiezo a cobrar la entrada al público, las probabilidades de que alguien salga lastimado son bastante bajas, a pesar de que no hay estándares rigurosos de ingeniería involucrados. Eso se basaría en un conocimiento general de estadística y mecánica.

En relación con su pregunta, diría que si está enviando código con un comportamiento indefinido, que nadie, ni el estándar, ni el fabricante del compilador, ni nadie más admitirá, eso es básicamente ingeniería de "tío loco jimbo", y es solo " está bien "si realiza una gran cantidad de pruebas para verificar que satisfaga sus necesidades, en base a un conocimiento general de estadísticas y computadoras.


Existe un problema fundamental con el comportamiento indefinido de cualquier tipo: se diagnostica mediante desinfectantes y optimizadores. Un compilador puede cambiar silenciosamente el comportamiento correspondiente a los de una versión a otra (por ejemplo, expandiendo su repertorio), y de repente tendrá un error imposible de rastrear en su programa. Esto debe ser evitado.

Sin embargo, hay un comportamiento indefinido que se hace "definido" por su implementación particular. Su máquina puede definir un desplazamiento a la izquierda por una cantidad negativa de bits, y sería seguro usarlo allí, ya que los cambios importantes de las características documentadas ocurren muy raramente. Un ejemplo más común es el alias estricto : GCC puede deshabilitar esta restricción con -fno-strict-aliasing .


Históricamente, los compiladores de C generalmente han tendido a actuar de una manera algo predecible, incluso cuando el Estándar no lo requiere. En la mayoría de las plataformas, por ejemplo, una comparación entre un puntero nulo y un puntero a un objeto muerto simplemente informará que no son iguales (útil si el código desea afirmar con seguridad que el puntero es nulo y atrapar si no lo es). El Estándar no requiere que los compiladores hagan estas cosas, pero históricamente los compiladores que podrían hacerlo fácilmente lo han hecho.

Desafortunadamente, algunos escritores de compiladores han tenido la idea de que si no se puede llegar a tal comparación mientras el puntero era válidamente no nulo, el compilador debería omitir el código de aserción. Peor aún, si también puede determinar que cierta entrada provocaría que se alcanzara el código con un puntero no nulo no válido, debería suponer que dicha entrada nunca se recibirá y omitir todo el código que manejaría dicha entrada.

Esperemos que este comportamiento del compilador resulte ser una moda pasajera. Supuestamente, está impulsado por el deseo de "optimizar" el código, pero para la mayoría de las aplicaciones la robustez es más importante que la velocidad, y hacer que los compiladores se enreden con el código que habría limitado el daño causado por entradas errantes o el comportamiento del programa de recados es una receta para el desastre.

Hasta entonces, sin embargo, uno debe tener mucho cuidado al usar compiladores para leer la documentación cuidadosamente, ya que no hay garantía de que un escritor de compiladores no haya decidido que era menos importante soportar comportamientos útiles que, aunque ampliamente respaldados, no son ordenado por el Estándar (como ser capaz de verificar de manera segura si dos objetos arbitrarios se superponen), que aprovechar cada oportunidad para eliminar el código que el Estándar no requiere que ejecute.


Los cambios en el sistema operativo, los cambios inocuos en el sistema (¡una versión de hardware diferente!) O los cambios en el compilador pueden causar que UB "funcional" previamente no funcione.

Pero es peor que eso.

A veces, un cambio a una unidad de compilación no relacionada, o un código lejano en la misma unidad de compilación, puede hacer que UB "funcional" previamente no funcione; Como ejemplo, dos funciones o métodos en línea con diferentes definiciones pero la misma firma. Uno se descarta silenciosamente durante el enlace; y los cambios de código completamente inocuos pueden cambiar cuál se descarta.

El código que funciona en un contexto puede dejar de funcionar repentinamente en el mismo compilador, sistema operativo y hardware cuando lo usa en un contexto diferente. Un ejemplo de esto es violar el alias fuerte; el código compilado podría funcionar cuando se llama en el lugar A, pero cuando está en línea (¡posiblemente en el momento del enlace!) el código puede cambiar el significado.

Su código, si forma parte de un proyecto más grande, podría llamar condicionalmente algún código de terceros (por ejemplo, una extensión de shell que previsualiza un tipo de imagen en un diálogo de apertura de archivo) que cambia el estado de algunos indicadores (precisión de punto flotante, configuración regional, desbordamiento de enteros banderas, división por cero comportamiento, etc. Su código, que funcionaba bien antes, ahora exhibe un comportamiento completamente diferente.

Luego, muchos tipos de comportamiento indefinido son inherentemente no deterministas. Acceder al contenido de un puntero después de liberarlo (incluso escribiéndole) puede ser seguro 99/100, pero 1/100 se cambió la página o se escribió algo más antes de llegar a ella. Ahora tienes corrupción de memoria. Pasa todas tus pruebas, pero carecías de un conocimiento completo de lo que puede salir mal.

Al utilizar un comportamiento indefinido, se compromete a comprender completamente el estándar C ++, todo lo que su compilador puede hacer en esa situación y todas las formas en que el entorno de ejecución puede reaccionar. ¡Debe auditar el ensamblado producido, no la fuente C ++, posiblemente para todo el programa, cada vez que lo construya! También compromete a todos los que leen ese código, o que modifican ese código, a ese nivel de conocimiento.

A veces todavía vale la pena.

Los delegados más rápidos posibles utilizan UB y el conocimiento sobre las convenciones de llamadas para ser un tipo std::function realmente rápido y no propietario.

Los delegados imposiblemente rápidos compiten. Es más rápido en algunas situaciones, más lento en otras y cumple con el estándar C ++.

Usar el UB podría valer la pena, para el aumento del rendimiento. Es raro que obtenga algo más que rendimiento (velocidad o uso de memoria) de tal piratería informática de UB.

Otro ejemplo que he visto es cuando tuvimos que registrar una devolución de llamada con una API de C pobre que solo tomó un puntero de función. Creamos una función (compilada sin optimización), la copiamos a otra página, modificamos una constante de puntero dentro de esa función, luego marcamos esa página como ejecutable, lo que nos permite pasar secretamente un puntero junto con el puntero de función a la devolución de llamada.

Una implementación alternativa sería tener un conjunto de funciones de tamaño fijo (10? 100? 1000? 1 millón?) Todas las cuales buscan una std::function en una matriz global y la invocan. Esto pondría un límite a la cantidad de devoluciones de llamada que instalamos en cualquier momento, pero prácticamente era suficiente.


No, eso no es seguro. En primer lugar, tendrá que arreglar todo , no solo la versión del compilador. No tengo ejemplos particulares, pero supongo que un sistema operativo diferente (actualizado) o incluso un procesador actualizado podría cambiar los resultados de UB.

Además, incluso tener una entrada de datos diferente a su programa puede cambiar el comportamiento de UB. Por ejemplo, un acceso de matriz fuera del límite (al menos sin optimizaciones) generalmente depende de lo que esté en la memoria después de la matriz. UPD : vea una gran respuesta de Yakk para más discusión sobre esto.

Y un problema mayor es la optimización y otros indicadores del compilador. UB puede manifestarse de diferentes maneras dependiendo de los indicadores de optimización, y es bastante difícil imaginar que alguien use siempre los mismos indicadores de optimización (al menos usará indicadores diferentes para depurar y liberar).

UPD : acabo de notar que nunca mencionaste arreglar una versión del compilador, solo mencionaste arreglar un compilador en sí. Entonces todo es aún más inseguro: las nuevas versiones del compilador definitivamente pueden cambiar el comportamiento de UB. De esta serie de publicaciones de blog :

Lo importante y aterrador es darse cuenta de que casi cualquier optimización basada en un comportamiento indefinido puede comenzar a activarse en un código defectuoso en cualquier momento en el futuro. La alineación, el desenrollado de bucles, la promoción de memoria y otras optimizaciones seguirán mejorando, y una parte importante de su razón para existir es exponer optimizaciones secundarias como las anteriores.


Piénselo de una manera diferente.

El comportamiento indefinido SIEMPRE es malo, y nunca se debe usar, porque nunca se sabe lo que se obtendrá.

Sin embargo, puedes moderar eso con

El comportamiento puede ser definido por otras partes además de la especificación del lenguaje

Por lo tanto, nunca debe confiar en UB, pero puede encontrar fuentes alternativas que indiquen que un determinado comportamiento es DEFINIDO para su compilador en sus circunstancias.

Yakk dio excelentes ejemplos con respecto a las clases rápidas de delegado. En esos casos, el autor afirma explícitamente que están participando en un comportamiento indefinido, de acuerdo con la especificación. Sin embargo, luego van a explicar una razón comercial por la cual el comportamiento está mejor definido que eso. Por ejemplo, declaran que es poco probable que el diseño de memoria de un puntero de función miembro cambie en Visual Studio porque habría costos comerciales desenfrenados debido a incompatibilidades que son desagradables para Microsoft. Por lo tanto, declaran que el comportamiento es "comportamiento definido de facto".

Se puede ver un comportamiento similar en la implementación típica de linux de pthreads (para ser compilada por gcc). Hay casos en los que hacen suposiciones sobre qué optimizaciones puede invocar un compilador en escenarios multiproceso. Esos supuestos se expresan claramente en los comentarios en el código fuente. ¿Cómo es este "comportamiento definido de facto"? Bueno, pthreads y gcc van de la mano. Se consideraría inaceptable agregar una optimización a gcc que rompió pthreads, por lo que nadie lo hará.

Sin embargo, no puede hacer la misma suposición. Puedes decir "pthreads lo hace, así que yo también debería poder hacerlo". Luego, alguien realiza una optimización y actualiza gcc para trabajar con él (tal vez usando llamadas __sync lugar de depender de volatile ). Ahora pthreads sigue funcionando ... pero su código ya no funciona.

Considere también el caso de MySQL (¿o fue Postgre?) Donde encontraron un error de desbordamiento del búfer. El desbordamiento había quedado atrapado en el código, pero lo hizo utilizando un comportamiento indefinido, por lo que el último gcc comenzó a optimizar todo el proceso de pago.

Entonces, en general, busque una fuente alternativa para definir el comportamiento, en lugar de usarlo mientras no está definido. Es totalmente legítimo encontrar una razón por la cual sabes que 1.0 / 0.0 es igual a NaN, en lugar de causar una trampa de punto flotante. Pero nunca use esa suposición sin probar primero que es una definición válida de comportamiento para usted y su compilador.

Y por favor, por favor, por favor, recuerde que actualizamos los compiladores de vez en cuando.


Si bien estoy de acuerdo con las respuestas que dicen que no es seguro, incluso si no se dirige a múltiples plataformas, cada regla puede tener excepciones.

Me gustaría presentar dos ejemplos en los que estoy seguro de que permitir un comportamiento indefinido / definido por la implementación fue la elección correcta.

  1. Un programa de un solo disparo. No es un programa destinado a ser utilizado por nadie, pero es un programa pequeño y escrito rápidamente creado para calcular o generar algo ahora . En tal caso, una solución "rápida y sucia" puede ser la elección correcta, por ejemplo, si conozco la resistencia de mi sistema y no quiero molestarme en escribir un código que funcione con la otra funcionalidad. Por ejemplo, solo lo necesitaba para realizar una prueba matemática para saber si podría usar una fórmula específica en mi otro programa orientado al usuario o no.

  2. Muy pequeños dispositivos integrados. Los microcontroladores más baratos tienen memoria medida en unos pocos cientos de bytes. Si desarrolla un juguete pequeño con LED parpadeantes o una postal musical, etc., cada centavo cuenta, porque se producirá en millones con un beneficio muy bajo por unidad. Ni el procesador ni el código cambian nunca, y si tiene que usar un procesador diferente para la próxima generación de su producto, probablemente tendrá que volver a escribir su código de todos modos. Un buen ejemplo de un comportamiento indefinido en este caso es que hay microcontroladores que garantizan un valor de cero (o 255) para cada ubicación de memoria en el encendido. En este caso, puede omitir la inicialización de sus variables. Si su microcontrolador tiene solo 256 bytes de memoria, esto puede marcar la diferencia entre un programa que se adapta a la memoria y un código que no.

Cualquiera que no esté de acuerdo con el punto 2, imagine lo que sucedería si le dijera algo así a su jefe:

"Sé que el hardware cuesta solo $ 0,40 y planeamos venderlo por $ 0,50. Sin embargo, el programa con 40 líneas de código que he escrito solo funciona para este tipo de procesador muy específico, por lo que si en un futuro lejano Si alguna vez cambia a un procesador diferente, el código no será utilizable y tendré que tirarlo y escribir uno nuevo. Un programa que cumpla con los estándares y que funcione para cada tipo de procesador no encajará en nuestro procesador de $ 0.40. Solicito usar un procesador que cuesta $ 0.60, porque me niego a escribir un programa que no sea portátil ".