Si tengo:
unsigned int x;
x -= x;
está claro que x
debería ser cero después de esta expresión, pero dondequiera que mire, dicen que el comportamiento de este código no está definido, no solo el valor de x
(hasta antes de la resta).
Dos preguntas:
¿El comportamiento de este código es realmente indefinido?
(Por ejemplo, ¿podría fallar el código [o peor] en un sistema compatible?)Si es así, ¿ por qué C dice que el comportamiento no está definido, cuando está perfectamente claro que
x
debería ser cero aquí?es decir, ¿cuál es la ventaja que se obtiene al no definir aquí el comportamiento?
Claramente, el compilador podría simplemente usar cualquier valor basura que considere "útil" dentro de la variable, y funcionaría según lo previsto ... ¿qué hay de malo en ese enfoque?
c
undefined-behavior
initialization
usuario541686
fuente
fuente
x -= x
. La pregunta es por qué acceder a valores no inicializados es UB.Respuestas:
Sí, este comportamiento no está definido, pero por razones diferentes a las que la mayoría de la gente conoce.
Primero, el uso de un valor unificado no es en sí mismo un comportamiento indefinido, pero el valor es simplemente indeterminado. Acceder a esto entonces es UB si el valor resulta ser una representación de trampa para el tipo. Los tipos sin firmar rara vez tienen representaciones de trampas, por lo que estaría relativamente seguro en ese lado.
Lo que hace que el comportamiento sea indefinido es una propiedad adicional de su variable, es decir, que "podría haber sido declarada con
register
", es decir, su dirección nunca se toma. Estas variables se tratan especialmente porque hay arquitecturas que tienen registros de CPU reales que tienen una especie de estado adicional que no está "no inicializado" y que no corresponde a un valor en el dominio de tipos.Editar: La frase relevante del estándar es 6.3.2.1p2:
Y para que quede más claro, el siguiente código es legal en todas las circunstancias:
unsigned char a, b; memcpy(&a, &b, 1); a -= a;
a
yb
, por lo que su valor es simplemente indeterminado.unsigned char
nunca ha habido representaciones de trampa en las que el valor indeterminado no está especificado, cualquier valor deunsigned char
podría suceder.a
debe contener el valor0
.Edit2:
a
yb
tienen valores no especificados:fuente
unsigned
seguro que pueden tener representaciones de trampas. ¿Puede señalar la parte del estándar que lo dice? Veo en §6.2.6.2 / 1 lo siguiente: "Para tipos de enteros sin signo distintos del carácter sin signo , los bits de la representación del objeto se dividirán en dos grupos: bits de valor y bits de relleno (no es necesario que haya ninguno de estos últimos). ... esto se conoce como la representación del valor. Los valores de cualquier bit de relleno no están especificados. ⁴⁴⁾ "con el comentario que dice:" ⁴⁴⁾ Algunas combinaciones de bits de relleno pueden generar representaciones de trampa ".unsigned char
, pero esta respuesta está usandounsigned char
. Sin embargo, tenga en cuenta: un programa estrictamente conforme puede calcularsizeof(unsigned) * CHAR_BIT
y determinar, basándose enUINT_MAX
, que implementaciones particulares no pueden tener representaciones de trampas paraunsigned
. Una vez que ese programa ha tomado esa determinación, puede proceder a hacer exactamente lo que hace esta respuestaunsigned char
.memcpy
una distracción, es decir, no se aplicaría su ejemplo si fuera reemplazado por*&a = *&b;
.unsigned char
quememcpy
ayuda y, por lo tanto , ayuda, el de*&
es menos claro. Informaré una vez que esto se calme.El estándar C da a los compiladores mucha libertad para realizar optimizaciones. Las consecuencias de estas optimizaciones pueden ser sorprendentes si asume un modelo ingenuo de programas donde la memoria no inicializada se establece en algún patrón de bits aleatorio y todas las operaciones se llevan a cabo en el orden en que se escriben.
Nota: los siguientes ejemplos solo son válidos porque
x
nunca se toma su dirección, por lo que es "similar a un registro". También serían válidos si el tipo dex
trampa tuviera representaciones; este es raramente el caso de los tipos sin firmar (requiere "desperdiciar" al menos un bit de almacenamiento y debe estar documentado), y es imposible paraunsigned char
. Six
tuviera un tipo firmado, entonces la implementación podría definir el patrón de bits que no es un número entre - (2 n-1 -1) y 2 n-1 -1 como una representación de trampa. Vea la respuesta de Jens Gustedt .Los compiladores intentan asignar registros a variables, porque los registros son más rápidos que la memoria. Dado que el programa puede usar más variables de las que tiene el procesador, los compiladores realizan la asignación de registros, lo que lleva a que diferentes variables utilicen el mismo registro en diferentes momentos. Considere el fragmento del programa
unsigned x, y, z; /* 0 */ y = 0; /* 1 */ z = 4; /* 2 */ x = - x; /* 3 */ y = y + z; /* 4 */ x = y + 1; /* 5 */
Cuando se evalúa la línea 3,
x
aún no se inicializa, por lo tanto (razona el compilador) la línea 3 debe ser una especie de casualidad que no puede suceder debido a otras condiciones que el compilador no fue lo suficientemente inteligente para resolver. Dadoz
que no se usa después de la línea 4 yx
no se usa antes de la línea 5, se puede usar el mismo registro para ambas variables. Entonces, este pequeño programa se compila para las siguientes operaciones en registros:r1 = 0; r0 = 4; r0 = - r0; r1 += r0; r0 = r1;
El valor final de
x
es el valor final der0
y el valor final dey
es el valor final der1
. Estos valores son x = -3 e y = -4, y no 5 y 4 como sucedería si sex
hubiera inicializado correctamente.Para obtener un ejemplo más elaborado, considere el siguiente fragmento de código:
unsigned i, x; for (i = 0; i < 10; i++) { x = (condition() ? some_value() : -x); }
Supongamos que el compilador detecta que
condition
no tiene efectos secundarios. Dadocondition
que no modificax
, el compilador sabe que la primera ejecución a través del bucle no puede tener accesox
ya que aún no está inicializado. Por lo tanto, la primera ejecución del cuerpo del bucle es equivalente ax = some_value()
, no es necesario probar la condición. El compilador puede compilar este código como si hubiera escritounsigned i, x; i = 0; /* if some_value() uses i */ x = some_value(); for (i = 1; i < 10; i++) { x = (condition() ? some_value() : -x); }
La forma en que esto se puede modelar dentro del compilador es considerar que cualquier valor que dependa de
x
tiene el valor que sea conveniente siempre quex
no esté inicializado. Debido a que el comportamiento cuando una variable no inicializada no está definida, en lugar de que la variable simplemente tenga un valor no especificado, el compilador no necesita realizar un seguimiento de ninguna relación matemática especial entre los valores convenientes. Por lo tanto, el compilador puede analizar el código anterior de esta manera:x
no se inicializa cuando-x
se evalúa el tiempo .-x
tiene un comportamiento indefinido, por lo que su valor es el que sea conveniente.condition ? value : value
condition; value
Cuando se enfrenta al código en su pregunta, este mismo compilador analiza que cuando
x = - x
se evalúa, el valor de-x
es lo que sea conveniente. Por lo que la asignación se puede optimizar.No he buscado un ejemplo de un compilador que se comporte como se describe arriba, pero es el tipo de optimizaciones que los buenos compiladores intentan hacer. No me sorprendería encontrarme con uno. Aquí hay un ejemplo menos plausible de un compilador con el que su programa falla. (Puede que no sea tan inverosímil si compila su programa en algún tipo de modo de depuración avanzada).
Este compilador hipotético mapea cada variable en una página de memoria diferente y configura los atributos de la página para que la lectura de una variable no inicializada provoque una trampa del procesador que invoca un depurador. Cualquier asignación a una variable primero asegura que su página de memoria esté mapeada normalmente. Este compilador no intenta realizar ninguna optimización avanzada; está en modo de depuración, destinado a localizar fácilmente errores como las variables no inicializadas. Cuando
x = - x
se evalúa, el lado derecho provoca una trampa y el depurador se activa.fuente
x
tiene un valor no inicializado pero el comportamiento al acceder sería definirse si x no tuvo un comportamiento similar al de un registro.x
, entonces todas las operaciones sobre él podrían omitirse independientemente de que su valor se haya definido o no. Si el código siguiente, por ejemploif (volatile1) x=volatile2; ... x = (x+volatile3) & 255;
, estaría igualmente satisfecho con cualquier valor 0-255 quex
pudiera contener en el caso de quevolatile1
hubiera arrojado cero, creo que una implementación que permitiría al programador omitir una escritura innecesariax
debería considerarse de mayor calidad que una que se comportaría ...Sí, el programa podría fallar. Puede haber, por ejemplo, representaciones de trampas (patrones de bits específicos que no se pueden manejar) que podrían causar una interrupción de la CPU, que si no se maneja podría bloquear el programa.
(Esta explicación solo se aplica en plataformas donde
unsigned int
pueden tener representaciones de trampas, lo cual es raro en los sistemas del mundo real; consulte los comentarios para obtener detalles y referencias a causas alternativas y quizás más comunes que conducen a la redacción actual del estándar).fuente
(Esta respuesta se refiere a C 1999. Para C 2011, consulte la respuesta de Jens Gustedt).
El estándar C no dice que usar el valor de un objeto de duración de almacenamiento automático que no está inicializado sea un comportamiento indefinido. El estándar C 1999 dice, en 6.7.8 10, "Si un objeto que tiene una duración de almacenamiento automático no se inicializa explícitamente, su valor es indeterminado". (Este párrafo continúa para definir cómo se inicializan los objetos estáticos, por lo que los únicos objetos no inicializados que nos preocupan son los objetos automáticos).
3.17.2 define "valor indeterminado" como "un valor no especificado o una representación trampa". 3.17.3 define “valor no especificado” como “valor válido del tipo relevante donde esta Norma Internacional no impone requisitos sobre qué valor se elige en cualquier caso”.
Entonces, si el no inicializado
unsigned int x
tiene un valor no especificado, entoncesx -= x
debe producir cero. Eso deja la pregunta de si puede ser una representación trampa. Acceder a un valor de trampa causa un comportamiento indefinido, según 6.2.6.1 5.Algunos tipos de objetos pueden tener representaciones de trampas, como los NaN de señalización de números de punto flotante. Pero los enteros sin signo son especiales. Según 6.2.6.2, cada uno de los N bits de valor de un int sin signo representa una potencia de 2, y cada combinación de los bits de valor representa uno de los valores de 0 a 2 N -1. Por lo tanto, los enteros sin signo pueden tener representaciones de trampa solo debido a algunos valores en sus bits de relleno (como un bit de paridad).
Si, en su plataforma de destino, un int sin signo no tiene bits de relleno, entonces un int sin firmar no inicializado no puede tener una representación de trampa y el uso de su valor no puede causar un comportamiento indefinido.
fuente
x
tiene una representación de trampa, entoncesx -= x
podría trampa, ¿verdad? Aún así, +1 para señalar números enteros sin firmar sin bits adicionales debe tener un comportamiento definido; es claramente lo opuesto a las otras respuestas y (según la cita) parece ser lo que implica el estándar.x
tiene una representación de trampa, entoncesx -= x
podría trampa. Incluso si sex
usa simplemente como valor podría atrapar. (Es seguro usarlox
como un valor l; la escritura en un objeto no se verá afectada por una representación de trampa que esté en él).Sí, no está definido. El código puede fallar. C dice que el comportamiento no está definido porque no hay una razón específica para hacer una excepción a la regla general. La ventaja es la misma que en todos los demás casos de comportamiento indefinido: el compilador no tiene que generar un código especial para que esto funcione.
¿Por qué crees que eso no sucede? Ese es exactamente el enfoque adoptado. El compilador no es necesario para que funcione, pero no es necesario para que falle.
fuente
x
podría declararse comoregister
, es decir, que nunca se toma su dirección. No sé si sabías eso (si lo estabas ocultando de manera efectiva) pero una respuesta correcta debe mencionarlo.Para cualquier variable de cualquier tipo, que no esté inicializada o por otras razones tenga un valor indeterminado, se aplica lo siguiente para la lectura de código de ese valor:
De lo contrario, si no hay representaciones de trampas, la variable toma un valor no especificado. No hay garantía de que este valor no especificado sea consistente cada vez que se lee la variable. Sin embargo, se garantiza que no será una representación de trampa y, por lo tanto, se garantiza que no invocará un comportamiento indefinido [3].
El valor se puede utilizar de forma segura sin provocar un bloqueo del programa, aunque dicho código no es portátil para sistemas con representaciones de trampa.
[1]: C11 6.3.2.1:
[2]: C11 6.2.6.1:
[3] C11:
fuente
stdint.h
que siempre debe usarse en lugar de los tipos nativos de C. Porquestdint.h
aplica el complemento a 2 y no los bits de relleno. En otras palabras, losstdint.h
tipos no pueden estar llenos de basura.Si bien muchas respuestas se enfocan en procesadores que atrapan el acceso a registros no inicializados, pueden surgir comportamientos extravagantes incluso en plataformas que no tienen tales trampas, utilizando compiladores que no hacen ningún esfuerzo particular para explotar UB. Considere el código:
volatile uint32_t a,b; uin16_t moo(uint32_t x, uint16_t y, uint32_t z) { uint16_t temp; if (a) temp = y; else if (b) temp = z; return temp; }
un compilador para una plataforma como ARM donde todas las instrucciones que no sean cargas y almacenes operan en registros de 32 bits podrían procesar razonablemente el código de una manera equivalente a:
volatile uint32_t a,b; // Note: y is known to be 0..65535 // x, y, and z are received in 32-bit registers r0, r1, r2 uin32_t moo(uint32_t x, uint32_t y, uint32_t z) { // Since x is never used past this point, and since the return value // will need to be in r0, a compiler could map temp to r0 uint32_t temp; if (a) temp = y; else if (b) temp = z & 0xFFFF; return temp; }
Si cualquiera de las lecturas volátiles arroja un valor distinto de cero, r0 se cargará con un valor en el rango 0 ... 65535. De lo contrario, producirá lo que contenía cuando se llamó a la función (es decir, el valor pasado a x), que podría no ser un valor en el rango 0..65535. El Estándar carece de terminología para describir el comportamiento del valor cuyo tipo es uint16_t pero cuyo valor está fuera del rango de 0..65535, excepto para decir que cualquier acción que pudiera producir tal comportamiento invoca UB.
fuente
uint16_t
, esa variable a veces puede leerse como 123 y, a veces, como 6553623). Si el resultado termina siendo ignorado ...register
, entonces puede tener bits adicionales que hacen que el comportamiento sea potencialmente indefinido. Eso es exactamente lo que estás diciendo, ¿verdad?