Esta publicación de Stack Overflow enumera una lista bastante completa de situaciones en las que la especificación del lenguaje C / C ++ declara que es un "comportamiento indefinido". Sin embargo, quiero entender por qué otros lenguajes modernos, como C # o Java, no tienen el concepto de "comportamiento indefinido". ¿Significa que el diseñador del compilador puede controlar todos los escenarios posibles (C # y Java) o no (C y C ++)?
50
nullptr
) no uno se molestó en definir el comportamiento escribiendo y / o adoptando una especificación propuesta ". : cRespuestas:
El comportamiento indefinido es una de esas cosas que fueron reconocidas como una muy mala idea solo en retrospectiva.
Los primeros compiladores fueron grandes logros y celebraron con júbilo las mejoras sobre la alternativa: lenguaje de máquina o programación en lenguaje ensamblador. Los problemas con eso eran bien conocidos, y se inventaron lenguajes de alto nivel específicamente para resolver esos problemas conocidos. (El entusiasmo en ese momento era tan grande que las HLL fueron a veces aclamadas como "el final de la programación", como si de ahora en adelante solo tuviéramos que escribir trivialmente lo que queríamos y el compilador haría todo el trabajo real).
No fue hasta más tarde que nos dimos cuenta de los nuevos problemas que surgieron con el nuevo enfoque. Estar alejado de la máquina real en la que se ejecuta el código significa que hay más posibilidades de que las cosas silenciosamente no hagan lo que esperábamos que hicieran. Por ejemplo, la asignación de una variable normalmente dejaría el valor inicial sin definir; esto no se consideró un problema, porque no asignaría una variable si no quisiera mantener un valor en ella, ¿verdad? Seguramente no era demasiado esperar que los programadores profesionales no olvidaran asignar el valor inicial, ¿verdad?
Resultó que con las bases de código más grandes y las estructuras más complicadas que se hicieron posibles con sistemas de programación más potentes, sí, muchos programadores cometerían tales descuidos de vez en cuando, y el comportamiento indefinido resultante se convirtió en un problema importante. Incluso hoy, la mayoría de las fugas de seguridad de pequeñas a horribles son el resultado de un comportamiento indefinido de una forma u otra. (La razón es que, por lo general, el comportamiento indefinido está de hecho muy definido por las cosas en el siguiente nivel inferior en informática, y los atacantes que entienden ese nivel pueden usar ese margen de maniobra para hacer que un programa no solo haga cosas no intencionadas, sino exactamente las cosas que tienen la intención.)
Desde que reconocimos esto, ha habido un impulso general para desterrar el comportamiento indefinido de los lenguajes de alto nivel, y Java fue particularmente cuidadoso al respecto (lo cual fue relativamente fácil ya que de todos modos fue diseñado para ejecutarse en su propia máquina virtual específicamente diseñada). Los lenguajes más antiguos como C no se pueden adaptar fácilmente sin perder la compatibilidad con la gran cantidad de código existente.
Editar: Como se señaló, la eficiencia es otra razón. El comportamiento indefinido significa que los escritores de compiladores tienen mucho margen de maniobra para explotar la arquitectura de destino para que cada implementación se salga con la implementación más rápida posible de cada característica. Esto fue más importante en las máquinas con poca potencia de ayer que hoy, cuando el salario del programador es a menudo el cuello de botella para el desarrollo de software.
fuente
int32_t add(int32_t x, int32_t y)
) en C ++. Los argumentos habituales en torno a ese están relacionados con la eficiencia, pero a menudo se intercalan con algunos argumentos de portabilidad (como en "Escribir una vez, ejecutar ... en la plataforma donde lo escribió ... y en ningún otro lugar ;-)"). Aproximadamente, un argumento podría ser: Algunas cosas no están definidas porque no sabes si estás en un microcontoller de 16 bits o en un servidor de 64 bits (uno débil, pero sigue siendo un argumento)Básicamente porque los diseñadores de Java y lenguajes similares no querían un comportamiento indefinido en su lenguaje. Esto fue una compensación: permitir un comportamiento indefinido tiene el potencial de mejorar el rendimiento, pero los diseñadores de lenguaje priorizaron la seguridad y la previsibilidad más alto.
Por ejemplo, si asigna una matriz en C, los datos no están definidos. En Java, todos los bytes deben inicializarse a 0 (o algún otro valor especificado). Esto significa que el tiempo de ejecución debe pasar sobre la matriz (una operación O (n)), mientras que C puede realizar la asignación en un instante. Entonces C siempre será más rápido para tales operaciones.
Si el código que usa la matriz se va a llenar de todos modos antes de leer, esto es básicamente un esfuerzo perdido para Java. Pero en el caso en el que el código se lee primero, obtienes resultados predecibles en Java pero resultados impredecibles en C.
fuente
valgrind
, que mostraría exactamente dónde se usó el valor no inicializado. No puede usarvalgrind
código java porque el tiempo de ejecución realiza la inicialización, lo que hace quevalgrind
las comprobaciones sean inútiles.El comportamiento indefinido permite una optimización significativa, al darle al compilador la libertad de hacer algo extraño o inesperado (o incluso normal) en ciertos límites u otras condiciones.
Ver http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
fuente
a + b
que se compile a laadd b a
instrucción nativa en cada situación, en lugar de requerir un compilador para simular alguna otra forma de aritmética de enteros con signo.HashSet
es maravilloso.<<
Podría ser el caso difícil.x << y
evalúa algún valor válido del tipoint32_t
pero no diremos cuál". Esto permite a los implementadores usar la solución rápida, pero no actúa como una precondición falsa que permite optimizaciones de estilo de viaje en el tiempo porque el no determinismo está limitado a la salida de esta operación: la especificación garantiza que la memoria, las variables volátiles, etc. no se vean afectadas por la expresión evaluación. ...En los primeros días de C, había mucho caos. Diferentes compiladores trataron el lenguaje de manera diferente. Cuando había interés en escribir una especificación para el lenguaje, esa especificación tendría que ser bastante compatible con el C que los programadores confiaban con sus compiladores. Pero algunos de esos detalles no son portables y no tienen sentido en general, por ejemplo, suponiendo una resistencia particular o diseño de datos. Por lo tanto, el estándar C reserva muchos detalles como comportamiento indefinido o específico de implementación, lo que deja mucha flexibilidad a los escritores de compiladores. C ++ se basa en C y también presenta un comportamiento indefinido.
Java trató de ser un lenguaje mucho más seguro y más simple que C ++. Java define la semántica del lenguaje en términos de una máquina virtual completa. Esto deja poco espacio para el comportamiento indefinido, por otro lado, impone requisitos que pueden ser difíciles de hacer para una implementación de Java (por ejemplo, que las asignaciones de referencia deben ser atómicas o cómo funcionan los enteros). Cuando Java admite operaciones potencialmente inseguras, la máquina virtual generalmente las verifica en tiempo de ejecución (por ejemplo, algunos conversiones).
fuente
this
nulo?" Hace un tiempo atrás, con el argumento de quethis
sernullptr
UB, y por lo tanto nunca puede suceder.)Los lenguajes JVM y .NET lo tienen fácil:
Sin embargo, hay buenos puntos para las opciones:
Cuando se proporcionan escotillas de escape, los que invitan a un comportamiento indefinido en toda regla vuelven a entrar. Pero, al menos, generalmente solo se usan en pocos tramos muy cortos, por lo que son más fáciles de verificar manualmente.
fuente
unsafe
palabra clave o atributos enSystem.Runtime.InteropServices
). Al mantener estas cosas a los pocos programadores que saben cómo depurar cosas no administradas y nuevamente tan poco como sea práctico, mantenemos los problemas bajos. Han pasado más de 10 años desde el último martillo inseguro relacionado con el rendimiento, pero a veces hay que hacerlo porque literalmente no hay otra solución.Java y C # se caracterizan por un proveedor dominante, al menos al principio de su desarrollo. (Sun y Microsoft respectivamente). C y C ++ son diferentes; Han tenido múltiples implementaciones competitivas desde el principio. C también funcionó especialmente en plataformas de hardware exóticas. Como resultado, hubo variación entre las implementaciones. Los comités ISO que estandarizaron C y C ++ podrían acordar un denominador común grande, pero en los bordes donde las implementaciones difieren, los estándares dejan espacio para la implementación.
Esto también se debe a que elegir un comportamiento puede ser costoso en arquitecturas de hardware que están sesgadas hacia otra opción: la endianidad es la opción obvia.
fuente
La verdadera razón se reduce a una diferencia fundamental en la intención entre C y C ++ por un lado, y Java y C # (por solo un par de ejemplos) por el otro. Por razones históricas, gran parte de la discusión aquí habla sobre C en lugar de C ++, pero (como probablemente ya sepa) C ++ es un descendiente bastante directo de C, por lo que lo que dice sobre C se aplica igualmente a C ++.
Aunque en gran parte se olvidan (y su existencia a veces incluso se niega), las primeras versiones de UNIX se escribieron en lenguaje ensamblador. Gran parte (si no únicamente) del propósito original de C era el puerto UNIX del lenguaje ensamblador a un lenguaje de nivel superior. Parte de la intención era escribir la mayor cantidad posible del sistema operativo en un lenguaje de nivel superior, o mirarlo desde la otra dirección, para minimizar la cantidad que tenía que escribirse en lenguaje ensamblador.
Para lograr eso, C necesitaba proporcionar casi el mismo nivel de acceso al hardware que el lenguaje ensamblador. El PDP-11 (por ejemplo) asignó registros de E / S a direcciones específicas. Por ejemplo, leería una ubicación de memoria para verificar si se presionó una tecla en la consola del sistema. Se estableció un bit en esa ubicación cuando había datos esperando ser leídos. Luego leería un byte de otra ubicación especificada para recuperar el código ASCII de la tecla que se había presionado.
Del mismo modo, si quisiera imprimir algunos datos, verificaría otra ubicación especificada y, cuando el dispositivo de salida estuviera listo, escribiría sus datos en otra ubicación especificada.
Para admitir la escritura de controladores para dichos dispositivos, C le permitió especificar una ubicación arbitraria utilizando algún tipo de entero, convertirlo en un puntero y leer o escribir esa ubicación en la memoria.
Por supuesto, esto tiene un problema bastante serio: no todas las máquinas en la tierra tienen su memoria idéntica a una PDP-11 de principios de los años setenta. Entonces, cuando tomas ese número entero, lo conviertes en un puntero y luego lees o escribes a través de ese puntero, nadie puede proporcionar ninguna garantía razonable sobre lo que vas a obtener. Solo por un ejemplo obvio, la lectura y la escritura pueden correlacionarse con registros separados en el hardware, por lo que usted (al contrario de la memoria normal) si escribe algo, intente leerlo de nuevo, lo que lea puede no coincidir con lo que escribió.
Puedo ver algunas posibilidades que deja:
De estos, 1 parece lo suficientemente absurdo como para que no valga la pena seguir discutiéndolo. 2 es básicamente tirar la intención básica del lenguaje Eso deja a la tercera opción como esencialmente la única que podrían considerar razonablemente.
Otro punto que surge con bastante frecuencia es el tamaño de los tipos enteros. C toma la "posición" que
int
debería ser el tamaño natural sugerido por la arquitectura. Entonces, si estoy programando un VAX de 32 bits,int
probablemente debería tener 32 bits, pero si estoy programando un Univac de 36 bits,int
probablemente debería tener 36 bits (y así sucesivamente). Probablemente no sea razonable (y puede que ni siquiera sea posible) escribir un sistema operativo para una computadora de 36 bits utilizando solo tipos que garanticen que sean múltiplos de 8 bits. Tal vez solo estoy siendo superficial, pero me parece que si estuviera escribiendo un sistema operativo para una máquina de 36 bits, probablemente querría usar un lenguaje que admitiera un tipo de 36 bits.Desde el punto de vista del lenguaje, esto conduce a un comportamiento aún más indefinido. Si tomo el valor más grande que cabe en 32 bits, ¿qué sucederá cuando agregue 1? En el hardware típico de 32 bits, se va a dar la vuelta (o posiblemente arroje algún tipo de falla de hardware). Por otro lado, si se ejecuta en hardware de 36 bits, solo ... agregará uno. Si el lenguaje va a admitir la escritura de sistemas operativos, no puede garantizar ninguno de los dos comportamientos: solo tiene que permitir que tanto el tamaño de los tipos como el comportamiento del desbordamiento varíen de uno a otro.
Java y C # pueden ignorar todo eso. No están destinados a admitir la escritura de sistemas operativos. Con ellos, tienes un par de opciones. Una es hacer que el hardware admita lo que exigen, ya que exigen tipos de 8, 16, 32 y 64 bits, solo construya hardware que admita esos tamaños. La otra posibilidad obvia es que el lenguaje solo se ejecute sobre otro software que proporciona el entorno que desean, independientemente de lo que el hardware subyacente pueda desear.
En la mayoría de los casos, esto no es realmente una opción o una opción. Más bien, muchas implementaciones hacen un poco de ambas. Normalmente ejecuta Java en una JVM que se ejecuta en un sistema operativo. La mayoría de las veces, el sistema operativo se escribe en C y la JVM en C ++. Si la JVM se ejecuta en una CPU ARM, es muy probable que la CPU incluya las extensiones Jazelle de ARM, para adaptar el hardware más de cerca a las necesidades de Java, por lo que hay que hacer menos en el software y el código Java se ejecuta más rápido (o menos lentamente, de todos modos).
Resumen
C y C ++ tienen un comportamiento indefinido, porque nadie ha definido una alternativa aceptable que les permita hacer lo que deben hacer. C # y Java adoptan un enfoque diferente, pero ese enfoque se ajusta mal (si es que lo hace) con los objetivos de C y C ++. En particular, ninguno de los dos parece proporcionar una manera razonable de escribir software de sistema (como un sistema operativo) en la mayoría del hardware elegido arbitrariamente. Ambos suelen depender de las instalaciones proporcionadas por el software del sistema existente (generalmente escrito en C o C ++) para hacer su trabajo.
fuente
Los autores de la Norma C esperaban que sus lectores reconocieran algo que pensaban que era obvio, y aludieron en su Justificación publicada, pero no dijeron directamente: el Comité no debería necesitar ordenar a los escritores de compiladores para satisfacer las necesidades de sus clientes, ya que los clientes deben saber mejor que el Comité cuáles son sus necesidades. Si es obvio que se espera que los compiladores para ciertos tipos de plataformas procesen una construcción de cierta manera, a nadie debería importarle si el Estándar dice que la construcción invoca Comportamiento indefinido. El hecho de que la Norma no exija que los compiladores conformes procesen una pieza de código de manera útil de ninguna manera implica que los programadores deberían estar dispuestos a comprar compiladores que no lo hagan.
Este enfoque del diseño del lenguaje funciona muy bien en un mundo donde los escritores de compiladores necesitan vender sus productos a clientes que pagan. Se desmorona por completo en un mundo donde los escritores de compiladores están aislados de los efectos del mercado. Es dudoso que existan las condiciones de mercado adecuadas para dirigir un idioma de la forma en que habían dirigido el que se hizo popular en la década de 1990, y aún más dudoso de que cualquier diseñador de idiomas sensato quisiera confiar en tales condiciones de mercado.
fuente
C ++ y c tienen estándares descriptivos (las versiones ISO, de todos modos).
Que solo existen para explicar cómo funcionan los idiomas y para proporcionar una referencia única sobre el idioma. Por lo general, los vendedores de compiladores y los escritores de bibliotecas lideran el camino y algunas sugerencias se incluyen en el estándar ISO principal.
Java y C # (o Visual C #, que supongo que quiere decir) tienen estándares prescriptivos . Te dicen definitivamente qué hay en el idioma con anticipación, cómo funciona y qué se considera comportamiento permitido.
Más importante que eso, Java en realidad tiene una "implementación de referencia" en Open-JDK. (Creo que Roslyn cuenta como la implementación de referencia de Visual C #, pero no pude encontrar una fuente para eso).
En el caso de Java, si hay alguna ambigüedad en el estándar, y Open-JDK lo hace de cierta manera. La forma en que Open-JDK lo hace es el estándar.
fuente
El comportamiento indefinido permite al compilador generar código muy eficiente en una variedad de arquitectos. La respuesta de Erik menciona la optimización, pero va más allá de eso.
Por ejemplo, los desbordamientos firmados son comportamientos indefinidos en C. En la práctica, se esperaba que el compilador generara un código de operación de suma firmado simple para que la CPU se ejecute, y el comportamiento sería lo que hiciera esa CPU en particular.
Eso permitió a C funcionar muy bien y producir código muy compacto en la mayoría de las arquitecturas. Si el estándar hubiera especificado que los enteros con signo debían desbordarse de cierta manera, entonces las CPU que se comportaban de manera diferente habrían necesitado mucha más generación de código para una simple adición firmada.
Esa es la razón de gran parte del comportamiento indefinido en C, y por qué cosas como el tamaño de
int
varían entre sistemas.Int
depende de la arquitectura y generalmente se selecciona para ser el tipo de datos más rápido y eficiente que es más grande que achar
.Cuando C era nuevo, estas consideraciones eran importantes. Las computadoras eran menos potentes, a menudo tenían una velocidad de procesamiento y memoria limitadas. C se usó donde el rendimiento realmente importaba, y se esperaba que los desarrolladores entendieran cómo funcionaban las computadoras lo suficientemente bien como para saber cuáles serían estos comportamientos indefinidos en sus sistemas particulares.
Los lenguajes posteriores como Java y C # prefirieron eliminar el comportamiento indefinido sobre el rendimiento sin procesar.
fuente
En cierto sentido, Java también lo tiene. Supongamos que le dio un comparador incorrecto a Arrays.sort. Puede arrojar excepción de lo detecta. De lo contrario, ordenará una matriz de alguna manera que no se garantiza que sea particular.
Del mismo modo, si modifica la variable de varios hilos, los resultados también son impredecibles.
C ++ fue más allá para crear más situaciones indefinidas (o más bien Java decidió definir más operaciones) y tener un nombre para ello.
fuente
a
sería un comportamiento indefinido si pudieras obtener 51 o 73, pero si solo puedes obtener 53 o 71, está bien definido.