¿Se pueden asumir las ramas con comportamiento indefinido inalcanzables y optimizarlas como código muerto?


88

Considere la siguiente declaración:

*((char*)NULL) = 0; //undefined behavior

Claramente invoca un comportamiento indefinido. ¿La existencia de tal declaración en un programa dado significa que todo el programa no está definido o que el comportamiento solo se vuelve indefinido una vez que el flujo de control llega a esta declaración?

¿El siguiente programa estaría bien definido en caso de que el usuario nunca ingrese el número 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

¿O es un comportamiento completamente indefinido sin importar lo que ingrese el usuario?

Además, ¿puede el compilador asumir que el comportamiento indefinido nunca se ejecutará en tiempo de ejecución? Eso permitiría razonar hacia atrás en el tiempo:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Aquí, el compilador podría razonar que en caso de num == 3que siempre invocaremos un comportamiento indefinido. Por lo tanto, este caso debe ser imposible y no es necesario imprimir el número. La ifdeclaración completa podría optimizarse. ¿Se permite este tipo de razonamiento al revés según el estándar?


19
a veces me pregunto si los usuarios con muchos representantes obtienen más votos a favor en las preguntas porque "tienen mucha reputación, esta debe ser una buena pregunta" ... pero en este caso leí la pregunta y pensé "wow, esto es genial "incluso antes de mirar al autor de la pregunta.
turbulencetoo

4
Creo que el momento en el que surge el comportamiento indefinido es indefinido.
eerorika

6
El estándar C ++ dice explícitamente que una ruta de ejecución con un comportamiento indefinido en cualquier punto es completamente indefinido. Incluso lo interpretaría como si dijera que cualquier programa con comportamiento indefinido en la ruta es completamente indefinido (eso incluye resultados razonables en otras partes, pero que no está garantizado). Los compiladores pueden utilizar el comportamiento indefinido para modificar su programa. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html contiene algunos buenos ejemplos.
Jens

4
@Jens: Realmente significa solo la ruta de ejecución. De lo contrario, te metes en problemas const int i = 0; if (i) 5/i;.
MSalters

1
El compilador en general no puede probar que PrintToConsoleno llama, std::exitpor lo que tiene que realizar la llamada.
MSalters

Respuestas:


65

¿La existencia de tal declaración en un programa dado significa que todo el programa no está definido o que el comportamiento solo se vuelve indefinido una vez que el flujo de control llega a esta declaración?

Ninguno. La primera condición es demasiado fuerte y la segunda es demasiado débil.

El acceso a objetos a veces se secuencia, pero el estándar describe el comportamiento del programa fuera del tiempo. Danvil ya citó:

si tal ejecución contiene una operación no definida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación no definida)

Esto se puede interpretar:

Si la ejecución del programa produce un comportamiento indefinido, entonces todo el programa tiene un comportamiento indefinido.

Entonces, una declaración inalcanzable con UB no le da al programa UB. Una declaración alcanzable que (debido a los valores de las entradas) nunca se alcanza, no le da al programa UB. Es por eso que su primera condición es demasiado fuerte.

Ahora bien, el compilador no puede decir en general qué tiene UB. Por lo tanto, para permitir que el optimizador reordene las declaraciones con UB potencial que podrían reordenarse si se definiera su comportamiento, es necesario permitir que UB "retroceda en el tiempo" y salga mal antes del punto de secuencia anterior (o en C ++ 11 terminología, para que la UB afecte a las cosas que están secuenciadas antes de la UB). Por lo tanto, su segunda condición es demasiado débil.

Un ejemplo importante de esto es cuando el optimizador se basa en un alias estricto. El objetivo de las reglas estrictas de alias es permitir al compilador reordenar operaciones que no podrían reordenarse válidamente si fuera posible que los punteros en cuestión alias la misma memoria. Entonces, si usa punteros de aliasing ilegalmente, y UB ocurre, entonces puede afectar fácilmente una instrucción "antes" de la instrucción UB. En lo que respecta a la máquina abstracta, la instrucción UB aún no se ha ejecutado. En lo que respecta al código de objeto real, se ha ejecutado total o parcialmente. Pero el estándar no intenta entrar en detalles sobre lo que significa para el optimizador reordenar declaraciones, o cuáles son las implicaciones de eso para UB. Simplemente le da la licencia de implementación para que funcione mal tan pronto como le plazca.

Puedes pensar en esto como, "UB tiene una máquina del tiempo".

Específicamente para responder a sus ejemplos:

  • El comportamiento solo está indefinido si se lee 3.
  • Los compiladores pueden eliminar el código como muerto si un bloque básico contiene una operación que seguramente no estará definida. Están permitidos (y supongo que sí) en casos que no son un bloque básico pero donde todas las ramas conducen a UB. Este ejemplo no es un candidato a menos que PrintToConsole(3)se sepa de alguna manera que esté seguro de regresar. Podría lanzar una excepción o lo que sea.

Un ejemplo similar al segundo es la opción gcc -fdelete-null-pointer-checks, que puede tomar un código como este (no he verificado este ejemplo específico, considérelo ilustrativo de la idea general):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

y cámbielo a:

*p = 3;
std::cout << "3\n";

¿Por qué? Porque si pes nulo, el código tiene UB de todos modos, por lo que el compilador puede asumir que no es nulo y optimizar en consecuencia. El kernel de Linux tropezó con esto ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) esencialmente porque opera en un modo en el que no se supone que desreferenciar un puntero nulo sea ​​UB, se espera que resulte en una excepción de hardware definida que el kernel pueda manejar. Cuando la optimización está habilitada, gcc requiere el uso de -fno-delete-null-pointer-checkspara proporcionar esa garantía más allá del estándar.

PD: La respuesta práctica a la pregunta "¿cuándo aparece el comportamiento indefinido?" es "10 minutos antes de que planeara partir por el día".


4
En realidad, hubo bastantes problemas de seguridad debido a esto en el pasado. En particular, cualquier verificación de desbordamiento posterior al hecho corre el riesgo de ser optimizada debido a esto. Por ejemplo, void can_add(int x) { if (x + 100 < x) complain(); }se puede optimizar por completo, porque si x+100 no se desborda, no pasa nada, y si x+100 se desborda, es UB según el estándar, por lo que no puede pasar nada .
fgp

3
@fgp: correcto, esa es una optimización de la que la gente se queja amargamente si se tropieza con ella, porque comienza a sentir que el compilador está rompiendo deliberadamente su código para castigarlo. "¿Por qué lo habría escrito de esa manera si quisiera que lo quitaras?" ;-) Pero creo que a veces es útil para el optimizador cuando manipula expresiones aritméticas más grandes, asumir que no hay desbordamiento y evitar cualquier cosa costosa que solo sería necesaria en esos casos.
Steve Jessop

2
¿Sería correcto decir que el programa no está indefinido si el usuario nunca ingresa 3, pero si ingresa 3 durante una ejecución, toda la ejecución queda indefinida? Tan pronto como sea 100% seguro que el programa invocará un comportamiento indefinido (y no antes de eso), se permitirá que el comportamiento sea cualquier cosa. ¿Son estas declaraciones mías 100% correctas?
usr

3
@usr: Creo que es correcto, sí. Con su ejemplo particular (y haciendo algunas suposiciones sobre la inevitabilidad de los datos que se procesan), creo que una implementación podría, en principio, mirar hacia adelante en STDIN almacenado en búfer para un 3si quisiera, y empacar en casa por el día tan pronto como lo viera entrante.
Steve Jessop

3
Un +1 adicional (si pudiera) para tu PD
Fred Larson

10

Los estados estándar en 1.9 / 4

[Nota: Esta Norma Internacional no impone requisitos sobre el comportamiento de los programas que contienen un comportamiento indefinido. - nota final]

El punto interesante es probablemente lo que significa "contener". Un poco más tarde en 1.9 / 5 dice:

Sin embargo, si tal ejecución contiene una operación indefinida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación indefinida).

Aquí menciona específicamente "ejecución ... con esa entrada". Lo interpretaría como que un comportamiento indefinido en una posible rama que no se ejecuta en este momento no influye en la rama de ejecución actual.

Sin embargo, un problema diferente son las suposiciones basadas en un comportamiento indefinido durante la generación de código. Vea la respuesta de Steve Jessop para obtener más detalles al respecto.


1
Si se toma literalmente, esa es la sentencia de muerte para todos los programas reales que existen.
usr

6
No creo que la pregunta fuera si UB puede aparecer antes de que se alcance el código. La pregunta, según lo entendí, era si la UB puede aparecer si ni siquiera se llega al código. Y, por supuesto, la respuesta es "no".
sepp2k

Bueno, el estándar no es tan claro sobre esto en 1.9 / 4, pero 1.9 / 5 posiblemente se pueda interpretar como lo que dijiste.
Danvil

1
Las notas no son normativas. 1.9 / 5 triunfa sobre la nota en 1.9 / 4
MSalters

5

Un ejemplo instructivo es

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Tanto el GCC actual como el Clang actual optimizarán esto (en x86) para

xorl %eax,%eax
ret

porque deducen que xsiempre es cero de la UB en la if (x)ruta de control. ¡GCC ni siquiera le dará una advertencia de uso de valor no inicializado! (porque el pase que aplica la lógica anterior se ejecuta antes del pase que genera advertencias de valor no inicializado)


1
Interesante ejemplo. Es bastante desagradable que habilitar la optimización oculte la advertencia. Esto ni siquiera está documentado: los documentos de GCC solo dicen que habilitar la optimización produce más advertencias.
sleske

@sleske Es desagradable, estoy de acuerdo, pero las advertencias de valor no inicializadas son notoriamente difíciles de "acertar" - hacerlas perfectamente es equivalente al Problema de Detención, y los programadores se vuelven extrañamente irracionales al agregar inicializaciones de variables "innecesarias" para silenciar los falsos positivos, por lo que los autores del compilador terminan siendo un barril. Solía ​​piratear GCC y recuerdo que todos tenían miedo de meterse con el pase de advertencia de valor no inicializado.
zwol

@zwol: Me pregunto cuánto de la "optimización" resultante de tal eliminación de código muerto en realidad hace que el código útil sea más pequeño, y cuánto termina haciendo que los programadores amplíen el código (por ejemplo, agregando código para inicializar aincluso si en todas las circunstancias uninitialized apasaría a la función que la función nunca haría nada con ella)?
supercat

@supercat No he estado profundamente involucrado en el trabajo del compilador en ~ 10 años, y es casi imposible razonar sobre optimizaciones a partir de ejemplos de juguetes. Este tipo de optimización tiende a asociarse con reducciones generales del tamaño del código del 2-5% en aplicaciones reales, si mal no recuerdo.
zwol

1
@supercat 2-5% es enorme a medida que avanzan estas cosas. He visto a la gente sudar un 0,1%.
zwol

4

El borrador de trabajo actual de C ++ dice en 1.9.4 que

Esta Norma Internacional no impone requisitos sobre el comportamiento de los programas que contienen un comportamiento indefinido.

En base a esto, diría que un programa que contiene un comportamiento indefinido en cualquier ruta de ejecución puede hacer cualquier cosa en cada momento de su ejecución.

Hay dos artículos realmente buenos sobre el comportamiento indefinido y lo que suelen hacer los compiladores:


1
Eso no tiene sentido. La función int f(int x) { if (x > 0) return 100/x; else return 100; }ciertamente nunca invoca un comportamiento indefinido, aunque, 100/0por supuesto, no está definido.
fgp

1
@fgp Lo que dice el estándar (especialmente 1.9 / 5), sin embargo, es que si se puede alcanzar un comportamiento indefinido , no importa cuándo se alcanza. Por ejemplo, printf("Hello, World"); *((char*)NULL) = 0 no se garantiza que imprima nada. Esto ayuda a la optimización, porque el compilador puede reordenar libremente las operaciones (sujeto a restricciones de dependencia, por supuesto) que sabe que ocurrirán eventualmente, sin tener que tomar en cuenta un comportamiento indefinido.
fgp

Yo diría que un programa con su función no contiene un comportamiento indefinido, porque no hay una entrada donde se evaluará 100/0.
Jens

1
Exactamente, entonces lo que importa es si la UB realmente se puede activar o no, no si teóricamente se puede activar. ¿O está preparado para argumentar que int x,y; std::cin >> x >> y; std::cout << (x+y);está permitido decir que "1 + 1 = 17", solo porque hay algunas entradas donde se x+ydesborda (que es UB ya que intes un tipo con signo)?
fgp

Formalmente, diría que el programa tiene un comportamiento indefinido porque existen entradas que lo activan. Pero tienes razón en que esto no tiene sentido en el contexto de C ++, porque sería imposible escribir un programa sin un comportamiento indefinido. Me gustaría cuando hubiera menos comportamiento indefinido en C ++, pero no es así como funciona el lenguaje (y hay algunas buenas razones para esto, pero no se refieren a mi uso diario ...).
Jens

3

La palabra "comportamiento" significa que se está haciendo algo . Un estadista que nunca se ejecuta no es "comportamiento".

Una ilustración:

*ptr = 0;

¿Es un comportamiento indefinido? Supongamos que estamos 100% seguros ptr == nullptral menos una vez durante la ejecución del programa. La respuesta deberia ser si.

¿Qué pasa con esto?

 if (ptr) *ptr = 0;

¿Eso es indefinido? (¿Se acuerda ptr == nullptral menos una vez?) Espero que no, de lo contrario no podrá escribir ningún programa útil.

Ningún srandardese resultó dañado en la elaboración de esta respuesta.


3

El comportamiento indefinido aparece cuando el programa causa un comportamiento indefinido sin importar lo que suceda a continuación. Sin embargo, dio el siguiente ejemplo.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

A menos que el compilador conozca la definición de PrintToConsole, no puede eliminar if (num == 3)condicional. Supongamos que tiene un LongAndCamelCaseStdio.hencabezado del sistema con la siguiente declaración de PrintToConsole.

void PrintToConsole(int);

Nada demasiado útil, de acuerdo. Ahora, veamos qué tan malvado (o quizás no tan malvado, el comportamiento indefinido podría haber sido peor) el proveedor, al verificar la definición real de esta función.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

El compilador en realidad tiene que asumir que cualquier función arbitraria que el compilador no sepa qué hace puede salir o lanzar una excepción (en el caso de C ++). Puede notar que *((char*)NULL) = 0;no se ejecutará, ya que la ejecución no continuará después de la PrintToConsolellamada.

El comportamiento indefinido ataca cuando PrintToConsolerealmente regresa. El compilador espera que esto no suceda (ya que esto haría que el programa ejecute un comportamiento indefinido pase lo que pase), por lo tanto, puede pasar cualquier cosa.

Sin embargo, consideremos algo más. Digamos que estamos haciendo una verificación nula y usamos la variable después de una verificación nula.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

En este caso, es fácil notar que lol_null_checkrequiere un puntero no NULL. La asignación a la warningvariable global no volátil no es algo que pueda salir del programa o generar alguna excepción. El pointertambién es no volátil, por lo que no puede cambiar mágicamente su valor en medio de la función (si lo hace, es un comportamiento indefinido). Llamar lol_null_check(NULL)provocará un comportamiento indefinido que puede hacer que la variable no sea asignada (porque en este punto, se conoce el hecho de que el programa ejecuta el comportamiento indefinido).

Sin embargo, el comportamiento indefinido significa que el programa puede hacer cualquier cosa. Por lo tanto, nada impide que el comportamiento indefinido retroceda en el tiempo y bloquee su programa antes de que se int main()ejecute la primera línea . Es un comportamiento indefinido, no tiene por qué tener sentido. También puede fallar después de escribir 3, pero el comportamiento indefinido retrocederá en el tiempo y fallará incluso antes de que escriba 3. Y quién sabe, tal vez un comportamiento indefinido sobrescriba la RAM de su sistema y haga que su sistema se bloquee 2 semanas después. mientras su programa indefinido no se esté ejecutando.


Todos los puntos válidos. PrintToConsolees mi intento de insertar un efecto secundario externo al programa que es visible incluso después de fallas y está fuertemente secuenciado. Quería crear una situación en la que podamos saber con certeza si esta declaración se optimizó. Pero tiene razón en que puede que nunca vuelva. Su ejemplo de escritura en un global podría estar sujeto a otras optimizaciones que no están relacionadas con UB. Por ejemplo, se puede eliminar un global no utilizado. ¿Tiene una idea para crear un efecto secundario externo de una manera que garantice que devuelva el control?
usr

¿Se pueden producir efectos secundarios observables en el mundo exterior mediante un código que un compilador podría asumir libremente? Según tengo entendido, incluso un método que simplemente lee una volatilevariable podría activar legítimamente una operación de E / S que a su vez podría interrumpir inmediatamente el hilo actual; el manejador de interrupciones podría entonces matar el hilo antes de que tenga la oportunidad de realizar cualquier otra cosa. No veo ninguna justificación por la cual el compilador pueda impulsar un comportamiento indefinido antes de ese punto.
supercat

Desde el punto de vista del estándar C, no habría nada ilegal en tener un comportamiento indefinido porque la computadora envía un mensaje a algunas personas que rastrearían y destruirían toda evidencia de las acciones previas del programa, pero si una acción pudiera terminar un hilo, entonces todo lo que se secuencia antes de esa acción tendría que suceder antes de cualquier Comportamiento Indefinido que ocurrió después de ella.
supercat

1

Si el programa llega a una declaración que invoca un comportamiento indefinido, no se imponen requisitos en ninguno de los resultados / comportamiento del programa; no importa si tendrán lugar "antes" o "después" de que se invoca un comportamiento indefinido.

Su razonamiento sobre los tres fragmentos de código es correcto. En particular, un compilador puede tratar cualquier declaración que invoca incondicionalmente un comportamiento indefinido de la forma en que GCC trata __builtin_unreachable(): como una sugerencia de optimización de que la declaración es inalcanzable (y, por lo tanto, que todas las rutas de código que conducen incondicionalmente a ella también son inalcanzables). Por supuesto, son posibles otras optimizaciones similares.


1
Por curiosidad, ¿cuándo __builtin_unreachable()empezaron a tener efectos que procedían tanto hacia atrás como hacia adelante en el tiempo? Dado algo como extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }, podría ver builtin_unreachable()que es bueno que el compilador sepa que puede omitir la returninstrucción, pero eso sería bastante diferente de decir que el código anterior podría omitirse.
supercat

@supercat dado que RESET_TRIGGER es volátil, la escritura en esa ubicación puede tener efectos secundarios arbitrarios. Para el compilador es como una llamada a un método opaco. Por tanto, no se puede probar (y no es el caso) que __builtin_unreachablese alcanza. Este programa está definido.
usr

@usr: Creo que los compiladores de bajo nivel deberían tratar los accesos volátiles como llamadas a métodos opacos, pero ni clang ni gcc lo hacen. Entre otras cosas, una llamada a un método opaco podría hacer que todos los bytes de cualquier objeto cuya dirección haya sido expuesta al mundo exterior, y que no haya sido ni será accedido por un restrictpuntero en vivo , se escriban usando un unsigned char*.
supercat

@usr: Si un compilador no trata un acceso volátil como una llamada a un método opaco con respecto a los accesos a objetos expuestos, no veo ninguna razón en particular para esperar que lo haga para otros propósitos. El estándar no requiere que las implementaciones lo hagan, porque hay algunas plataformas de hardware en las que un compilador podría conocer todos los efectos posibles de un acceso volátil. Sin embargo, un compilador adecuado para uso integrado debe reconocer que los accesos volátiles pueden activar hardware que no se había inventado cuando se escribió el compilador.
supercat

@supercat, creo que tienes razón. Parece que las operaciones volátiles "no tienen ningún efecto en la máquina abstracta" y, por lo tanto, no pueden terminar el programa ni causar efectos secundarios.
usr

1

Muchos estándares para muchos tipos de cosas gastan mucho esfuerzo en describir cosas que las implementaciones DEBEN o NO DEBEN hacer, usando una nomenclatura similar a la definida en IETF RFC 2119 (aunque no necesariamente citando las definiciones en ese documento). En muchos casos, las descripciones de las cosas que deberían hacer las implementaciones, excepto en los casos en que serían inútiles o poco prácticas, son más importantes que los requisitos a los que deben ajustarse todas las implementaciones conformes.

Desafortunadamente, los estándares C y C ++ tienden a evitar las descripciones de cosas que, aunque no se requieren al 100%, deberían esperarse de implementaciones de calidad que no documentan el comportamiento contrario. Una sugerencia de que las implementaciones deberían hacer algo podría verse como una implicación de que aquellas que no lo hacen son inferiores, y en los casos en los que generalmente sería obvio qué comportamientos serían útiles o prácticos, en lugar de imprácticos e inútiles, en una implementación dada, había poca necesidad percibida de que la Norma interfiera con tales juicios.

Un compilador inteligente podría ajustarse al Estándar y eliminar cualquier código que no tendría ningún efecto excepto cuando el código recibe entradas que inevitablemente causarían un comportamiento indefinido, pero "inteligente" y "tonto" no son antónimos. El hecho de que los autores del Estándar decidieran que podría haber algunos tipos de implementaciones en las que comportarse de manera útil en una situación dada sería inútil y poco práctico no implica ningún juicio sobre si tales comportamientos deben considerarse prácticos y útiles para otros. Si una implementación pudiera mantener una garantía de comportamiento sin costo alguno más allá de la pérdida de una oportunidad de poda de "rama muerta", casi cualquier valor que el código de usuario pudiera recibir de esa garantía excedería el costo de proporcionarlo. La eliminación de la rama muerta puede estar bien en los casos en que no lo haría ', pero si en una situación dada el código de usuario podría haber manejado casi cualquier comportamiento posible que no sea ​​la eliminación de ramas muertas, cualquier esfuerzo que el código de usuario tendría que gastar para evitar UB probablemente excedería el valor logrado de DBE.


Es un buen punto que evitar UB puede imponer un costo en el código de usuario.
usr

@usr: Es un punto que los modernistas olvidan por completo. ¿Debo agregar un ejemplo? Por ejemplo, si el código necesita evaluar x*y < zcuándo x*yno se desborda, y en caso de desbordamiento produce 0 o 1 de manera arbitraria pero sin efectos secundarios, no hay ninguna razón en la mayoría de las plataformas por la que cumplir con el segundo y tercer requisitos debería ser más costoso que cumplir con el primero, pero cualquier forma de escribir la expresión para garantizar el comportamiento definido por el Estándar en todos los casos agregaría un costo significativo en algunos casos. Escribir la expresión como (int64_t)x*y < zpodría más que cuadriplicar el costo de cálculo ...
supercat

... en algunas plataformas, y escribirlo como (int)((unsigned)x*y) < zevitaría que un compilador empleara lo que de otra manera podrían haber sido sustituciones algebraicas útiles (por ejemplo, si sabe que xy zson iguales y positivos, podría simplificar la expresión original a y<0, pero la versión que usa unsigned obligaría al compilador a realizar la multiplicación). Si el compilador puede garantizar que aunque el Estándar no lo exija, mantendrá el requisito de "rendimiento 0 o 1 sin efectos secundarios", el código de usuario podría brindarle al compilador oportunidades de optimización que de otro modo no podría obtener.
supercat

Sí, parece que alguna forma más leve de comportamiento indefinido sería útil aquí. El programador podría activar un modo que provoque la x*yemisión de un valor normal en caso de desbordamiento, pero cualquier valor. UB configurable en C / C ++ me parece importante.
usr

@usr: Si los autores del Estándar C89 fueran sinceros al decir que la promoción de valores cortos sin firmar para iniciar sesión era el cambio más serio, y no eran tontos ignorantes, eso implicaría que esperaban eso en los casos en que las plataformas habían estado definiendo garantías de comportamiento útiles, las implementaciones para tales plataformas habían estado poniendo esas garantías a disposición de los programadores, y los programadores las habían estado explotando, los compiladores para tales plataformas continuarían ofreciendo tales garantías de comportamiento tanto si el Estándar lo ordenaba como si no .
supercat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.