c++ undefined-behavior

c++ - ¿Vale la pena el comportamiento indefinido?



undefined-behavior (11)

Muchas cosas malas sucedieron y continúan sucediendo (o no, quién sabe, cualquier cosa puede suceder) debido a un comportamiento indefinido. Entiendo que esto se introdujo para dejar algo de margen de maniobra para que los compiladores puedan optimizar, y quizás también para hacer que C ++ sea más fácil de migrar a diferentes plataformas y arquitecturas. Sin embargo, los problemas causados ​​por un comportamiento indefinido parecen ser demasiado grandes para ser justificados por estos argumentos. ¿Cuáles son otros argumentos para el comportamiento indefinido? Si no hay ninguno, ¿por qué sigue existiendo un comportamiento indefinido?

Editar Para agregar algo de motivación a mi pregunta: debido a varias malas experiencias con menos C ++: compañeros de trabajo astutos, me he acostumbrado a hacer que mi código sea lo más seguro posible. Afirmar todos los argumentos, constricciones rigurosas y cosas por el estilo. Trato de dejar lo más pequeño que pueda el espacio para usar mi código de manera incorrecta, porque la experiencia demuestra que, si hay lagunas, la gente los usará y luego me llamarán porque mi código es malo. Considero que mi código sea lo más seguro posible como una buena práctica. Es por esto que no entiendo por qué existe un comportamiento indefinido. ¿Puede alguien darme un ejemplo de comportamiento indefinido que no pueda detectarse en tiempo de ejecución o de compilación sin una sobrecarga considerable?


Aquí está mi favorito: después de que hayas terminado de delete en un puntero que no sea nulo usándolo (no solo la desreferenciación, sino también castin, etc.) es UB (consulta esta pregunta) .

Cómo te puedes encontrar con UB:

{ char* pointer = new char[10]; delete[] pointer; // some other code printf( "deleted %x/n", pointer ); }

Ahora, en todas las arquitecturas, sé que el código anterior funcionará bien. Enseñar al compilador o al tiempo de ejecución a realizar un análisis de tales situaciones es muy difícil y costoso. No olvide que a veces puede haber millones de líneas de código entre delete y usar el puntero. La configuración de los punteros a nulos inmediatamente después de la delete puede ser costosa, por lo que tampoco es una solución universal.

Por eso está el concepto de UB. No quieres UB en tu código. Tal vez funciona tal vez no. Trabaja en esta implementación, rompe en otra.


Creo que el corazón de la preocupación proviene de la filosofía de velocidad C / C ++ sobre todo.

Estos idiomas se crearon en un momento en que la potencia bruta era escasa y necesitaba obtener todas las optimizaciones que podría tener para poder usar algo.

Especificar cómo tratar con UB significaría detectarla en primer lugar y luego, por supuesto, especificar el manejo adecuado. Sin embargo, la detección está en contra de la velocidad de la filosofía de los idiomas!

Hoy, ¿todavía necesitamos programas rápidos? Sí, para aquellos de nosotros que trabajamos con recursos muy limitados (sistemas integrados) o con restricciones muy severas (en el tiempo de respuesta o en las transacciones por segundo), necesitamos exprimir todo lo que podamos.

Sé que el lema arroja más hardware al problema . Tenemos una aplicación donde trabajo:

  • tiempo esperado para una respuesta? Menos de 100 ms, con llamadas de base de datos en medio (gracias a memcached).
  • número de transacciones por segundo? 1200 en promedio, picos a 1500/1700.

Se ejecuta en aproximadamente 40 monstruos: 8 opteron de doble núcleo (2800MHz) con 32GB de RAM. Se vuelve difícil ser "más rápido" con más hardware en este punto, por lo que necesitamos un código optimizado y un lenguaje que lo permita (nos restringimos a lanzar el código de ensamblaje allí).

Debo decir que de todas formas no me importa mucho la UB. Si llega al punto en que su programa invoca a UB, entonces necesita corregir cualquier comportamiento que haya ocurrido realmente. Por supuesto, sería más fácil solucionarlos si se informara de inmediato: para eso son las compilaciones de depuración.

Así que quizás, en lugar de centrarnos en UB, deberíamos aprender a usar el lenguaje:

  • no use llamadas no verificadas
  • (para expertos) no usar llamadas no verificadas
  • (para los gurús) ¿estás seguro de que realmente necesitas una llamada no verificada aquí?

Y todo es de repente mejor :)


El estándar deja el comportamiento "cierto" indefinido para permitir una variedad de implementaciones, sin cargar esas implementaciones con la sobrecarga de detectar "ciertas" situaciones, o cargar al programador con las restricciones necesarias para evitar que esas situaciones surjan en primer lugar.

Hubo un tiempo en el que evitar esta sobrecarga era una gran ventaja de C y C ++ para una amplia gama de proyectos.

Las computadoras ahora son varios miles de veces más rápidas de lo que eran cuando se inventó C, y los gastos generales de cosas como verificar los límites de los arreglos todo el tiempo, o tener unos pocos megabytes de código para implementar un tiempo de ejecución de espacio aislado, no parecen ser un gran problema para La mayoría de los proyectos. Además, el costo de (por ejemplo) sobrepasar un búfer ha aumentado por varios factores, ahora que nuestros programas manejan muchos megabytes de datos potencialmente maliciosos por segundo.

Por lo tanto, es algo frustrante que no haya un lenguaje que tenga todas las características útiles de C ++, y que además tenga la propiedad de que se define el comportamiento de cada programa que compila (sujeto al comportamiento específico de la implementación). Pero solo un poco, en realidad no es tan difícil en Java escribir código cuyo comportamiento sea tan confuso que desde el punto de vista de la depuración, si no es la seguridad, bien podría no estar definido. Tampoco es difícil escribir código Java inseguro, solo que la inseguridad suele limitarse a filtrar información confidencial u otorgar privilegios incorrectos sobre la aplicación, en lugar de ceder el control completo del proceso del sistema operativo en el que se ejecuta la JVM.

Entonces, la forma en que lo veo es que una buena ingeniería de software requiere disciplina en todos los idiomas, la diferencia es lo que sucede cuando nuestra disciplina falla, y cuánto nos cobran otros idiomas (en características de desempeño y huella y C ++ que le gustan) para el seguro contra eso Si el seguro proporcionado por algún otro idioma vale la pena para su proyecto, tómelo. Si vale la pena pagar por las funciones proporcionadas por C ++ con el riesgo de un comportamiento indefinido, tome C ++. No creo que haya mucho kilometraje al tratar de argumentar, como si fuera una propiedad global que es igual para todos, ya sea que los beneficios de C ++ "justifiquen" los costos. Están justificados dentro de los términos de referencia para el diseño del lenguaje C ++, que es que usted no paga por lo que no usa. Por lo tanto, los programas correctos no deben hacerse más lentos para que los programas incorrectos reciban un mensaje de error útil en lugar de UB, y la mayor parte del tiempo el comportamiento en casos inusuales (por ejemplo, << 32 de un valor de 32 bits) no debe definirse (por ejemplo, para dar como resultado 0) si eso requiere que se verifique el caso inusual explícitamente en el hardware que el comité quiere apoyar a C ++ "eficientemente".

Veamos otro ejemplo: no creo que los beneficios de rendimiento del compilador profesional C y C ++ de Intel justifiquen el costo de comprarlo. Por lo tanto, no lo he comprado. No significa que otros harán el mismo cálculo que hice, o que siempre haré el mismo cálculo en el futuro.


Es importante dejar en claro las diferencias entre el comportamiento indefinido y el comportamiento definido por la implementación. El comportamiento de implementación definido le brinda a los escritores de compiladores la oportunidad de agregar extensiones al lenguaje para aprovechar su plataforma. Tales extensiones son necesarias para escribir código que funcione en el mundo real.

Por otra parte, UB existe en los casos en que es difícil o imposible diseñar una solución sin imponer cambios importantes en el idioma o grandes diferencias con C. Un ejemplo tomado de una página donde BS habla de esto es:

int a[10]; a[100] = 0; // range error int* p = a; // ... p[100] = 0; // range error (unless we gave p a better value before that assignment)

El error de rango es UB. Es un error, pero la Norma no puede definir la precisión con la que la plataforma debe lidiar con esto. Cada plataforma es diferente. No se puede convertir en un error debido a que esto requeriría incluir la verificación automática del rango en el idioma, lo que requeriría un cambio importante en el conjunto de características del idioma. El error p[100] = 0 es aún más difícil para el lenguaje generar un diagnóstico, ya sea en tiempo de compilación o ejecución, porque el compilador no puede saber a qué apunta realmente p sin soporte en tiempo de ejecución.


Hay momentos en que el comportamiento indefinido es bueno. Tomemos un gran int, por ejemplo.

union BitInt { __int64 Whole; struct { int Upper; int Lower; // or maybe it''s lower upper. Depends on architecture } Parts; };

La especificación dice que si la última vez que leímos o escribimos a Whole, entonces la lectura / escritura de Partes no está definida.

Ahora, eso es solo un poco tonto para mí porque si no pudiéramos tocar otras partes del sindicato, entonces no tiene sentido tener el sindicato en primer lugar, ¿verdad?

Pero de todos modos, tal vez algunas funciones tomarán __int64 mientras que otras funciones tomarán las dos entradas separadas. En lugar de convertir cada vez que podemos usar esta unión. Cada compilador que conozco trata este comportamiento indefinido de una manera bastante clara. Así que en mi opinión, el comportamiento indefinido no es tan malo aquí.


La principal fuente de comportamiento indefinido son los punteros, y es por eso que C y C ++ tienen muchos comportamientos indefinidos.

Considere este código:

char * r = 0x012345ff; std::cout << r;

Este código se ve muy mal, pero ¿debería emitir un error? ¿Qué pasa si esa dirección es realmente legible, es decir, es un valor que obtuve de alguna manera (tal vez una dirección de dispositivo, etc.)?

En casos como este, no hay manera de saber si la operación es legal o no, y si no lo es, su comportamiento es de hecho impredecible.

Aparte de esto, en general, C ++ se diseñó teniendo en mente "La regla de sobrecarga cero" (consulte Diseño y evolución de C ++ ), por lo que no podría imponer ninguna carga en las implementaciones para verificar casos de esquina, etc. Siempre debe mantener Tenga en cuenta que este lenguaje se diseñó y, de hecho, se usa no solo en el escritorio sino también en sistemas integrados con recursos limitados.


Los compiladores y los lenguajes de programación son uno de mis temas favoritos. En el pasado hice algunas investigaciones relacionadas con compiladores y encontré muchas veces conductas indefinidas .

C ++ y Java son muy populares. No significa que tengan un gran diseño. Se usan ampliamente porque asumieron riesgos en detrimento de la calidad de su diseño solo para ganar aceptación. Java optó por la recolección de basura, la máquina virtual y la apariencia sin punteros. Fueron los pioneros en parte y no pudieron aprender de muchos proyectos anteriores.

En el caso de C ++, uno de los objetivos principales era proporcionar programación orientada a objetos a los usuarios de C. Incluso los programas en C deberían compilarse con un compilador de C ++. Eso hizo muchos puntos abiertos desagradables y C ya tenía muchas ambigüedades. El énfasis de C ++ era poder y popularidad, no integridad. No muchos idiomas le dan herencia múltiple, C ++ le da eso aunque no de una manera muy pulida. El comportamiento indefinido siempre estará ahí para apoyar su gloria y compatibilidad con versiones anteriores.

Si realmente desea un lenguaje robusto y bien definido, debe buscar en otro lugar. Lamentablemente esa no es la principal preocupación de la mayoría de las personas. Ada, por ejemplo, es un gran lenguaje donde un comportamiento claro y definido es importante, pero a casi nadie le importa el idioma debido a su limitada base de usuarios. Estoy sesgado con el ejemplo porque realmente me gusta ese idioma, publiqué algo en mi blog, pero si desea obtener más información sobre cómo una definición de idioma puede ayudar a tener menos errores, incluso antes de compilar, eche un vistazo a estas diapositivas.

¡No estoy diciendo que C ++ sea un mal lenguaje! Simplemente tiene diferentes objetivos y me encanta trabajar con él. También tienes una gran comunidad, excelentes herramientas y muchas más cosas geniales como STL, Boost y QT. Pero su duda también es la raíz para convertirse en un gran programador de C ++. Si quieres ser genial con C ++, esta debería ser una de tus preocupaciones. Le animo a leer las diapositivas anteriores y también a este crítico . Le ayudará mucho comprender esos momentos en que el idioma no está haciendo lo que usted espera.

Y por cierto. El comportamiento indefinido va totalmente contra la portabilidad. En Ada, por ejemplo, usted tiene control sobre el diseño de las estructuras de datos (en C y C ++ puede cambiar según la máquina y el compilador). Los hilos son parte del lenguaje. Por lo tanto, portar el software C y C ++ te dará más dolor que placer


Los problemas no son causados ​​por un comportamiento indefinido, son causados ​​por escribir el código que conduce a él. La respuesta es simple: no escriba ese tipo de código; no hacerlo no es exactamente una ciencia de cohetes.

Como para:

un ejemplo de comportamiento indefinido que no se puede detectar en tiempo de ejecución o de compilación sin una sobrecarga considerable

Un problema del mundo real:

int * p = new int; // call loads of stuff which may create an alias to p called q delete p; // call more stuff, somewhere in which you do: delete q;

Detectar esto en tiempo de compilación es imposible. en el tiempo de ejecución es simplemente extremadamente difícil y requeriría que el sistema de asignación de memoria realice mucha más contabilidad (es decir, sea más lento y ocupe más memoria) de lo que es el caso si simplemente decimos que la segunda eliminación no está definida. Si no te gusta esto, quizás C ++ no sea el idioma para ti, ¿por qué no cambiar a java?


Me hice esa misma pregunta hace unos años. Dejé de considerarlo de inmediato, cuando intenté proporcionar una definición adecuada para el comportamiento de una función que escribe en un puntero nulo.

No todos los dispositivos tienen un concepto de memoria protegida. Por lo tanto, no es posible confiar en el sistema para protegerlo a través de un fallo de seguridad o similar. No todos los dispositivos tienen memoria de solo lectura, por lo que posiblemente no se puede decir que la escritura simplemente no hace nada. La única otra opción que se me ocurre es requerir que la aplicación genere una excepción [o aborte, o algo así] sin la ayuda del sistema. Pero en ese caso, el compilador tiene que insertar un código antes de cada escritura de memoria para verificar si es nulo, a menos que pueda garantizar que el puntero no haya cambiado desde la escritura de la memoria de lista. Eso es claramente inaceptable.

Por lo tanto, dejar el comportamiento indefinido fue la única decisión lógica que pude tomar, sin decir que "los compiladores compatibles con C ++ solo se pueden implementar en plataformas con memoria protegida".


Mi opinión sobre el comportamiento indefinido es la siguiente:

El estándar define cómo se utilizará el lenguaje y cómo se supone que la implementación debe reaccionar cuando se usa de la manera correcta. Sin embargo, sería mucho trabajo cubrir todos los usos posibles de cada característica, por lo que el estándar simplemente lo deja así.

Sin embargo, en una implementación de compilador, no puede simplemente "dejarlo así", el código debe convertirse en instrucciones de la máquina, y no puede dejar espacios en blanco. En muchos casos, el compilador puede generar un error, pero eso no siempre es factible: hay algunos casos en los que sería necesario un trabajo extra para verificar si el programador está haciendo algo incorrecto (por ejemplo: llamar a un destructor dos veces, para detectar esto). , el compilador tendría que contar cuántas veces se han llamado ciertas funciones, o agregar un estado extra, o algo así. Entonces, si el estándar no lo define, y el compilador simplemente deja que suceda, a veces suceden cosas ingeniosas, tal vez, si tienes mala suerte.


Muchas cosas que se definen como comportamiento indefinido serían difíciles si no imposibles de diagnosticar por el compilador o el entorno de ejecución.

Los que son fáciles ya se han convertido en comportamientos definidos y no definidos . Considere llamar a un método virtual puro: es un comportamiento indefinido, pero la mayoría de los entornos de compilación / tiempo de ejecución proporcionarán un error en los mismos términos: se llama método virtual puro . El estándar de facto es que llamar a un método virtual puro es un error de tiempo de ejecución en todos los entornos que conozco.