(¿Por qué) está utilizando un comportamiento indefinido de variable no inicializada?

Si tengo:

unsigned int x; x -= x; 

Está claro que x debe ser cero después de esta expresión, pero en todas partes que miro, dicen que el comportamiento de este código no está definido, no simplemente el valor de x (hasta antes de la resta).

Dos preguntas:

  • ¿El comportamiento de este código no está definido?
    (Por ejemplo, ¿podría bloquearse el código [o algo peor] en un sistema compatible?)

  • Si es así, ¿ por qué C dice que el comportamiento no está definido, cuando está perfectamente claro que x debe ser cero aquí?

    es decir, ¿cuál es la ventaja dada al no definir el comportamiento aquí?

Claramente, el comstackdor simplemente podría usar cualquier valor de basura que considere “útil” dentro de la variable, y funcionaría según lo previsto … ¿qué hay de malo con ese enfoque?

Sí, este comportamiento no está definido, pero por razones diferentes a las que la mayoría de la gente conoce.

En primer lugar, usar un valor unitario no es, en sí mismo, un comportamiento indefinido, sino que el valor es simplemente indeterminado. Accediendo a esto, entonces es UB si el valor pasa a 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, a saber, que “podría haber sido declarado con register “, es decir, su dirección nunca se toma. Tales variables se tratan especialmente porque hay architectures que tienen registros de CPU reales que tienen un tipo de estado extra que está “sin inicializar” y que no se corresponde con un valor en el dominio de tipo.

Editar: la frase relevante del estándar es 6.3.2.1p2:

Si lvalue designa un objeto de duración de almacenamiento automático que podría haberse declarado con la clase de almacenamiento de registros (nunca se tomó su dirección), y ese objeto no se inicializó (no se declaró con un inicializador y no se realizó ninguna asignación antes del uso ), el comportamiento no está definido.

Y para hacerlo más claro, el siguiente código es legal bajo todas las circunstancias:

 unsigned char a, b; memcpy(&a, &b, 1); a -= a; 
  • Aquí se toman las direcciones de a y b , por lo que su valor es simplemente indeterminado.
  • Como el unsigned char nunca tiene representaciones de trapping que indiquen que el valor indeterminado no está especificado, cualquier valor de unsigned char podría ocurrir.
  • Al final, debe contener el valor 0 .

Edit2: a y b tienen valores no especificados:

3.19.3 valor no especificado
valor válido del tipo relevante donde esta Norma Internacional no impone requisitos sobre qué valor se elige en cualquier instancia

El estándar C brinda a los comstackdores mucha libertad para realizar optimizaciones. Las consecuencias de estas optimizaciones pueden ser sorprendentes si se asume un modelo ingenuo de progtwigs en el que la memoria no inicializada se establece en un 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 tiene su dirección tomada, por lo que es “similar a un registro”. También serían válidos si el tipo de x tuviera representaciones de trampas; esto rara vez es el caso para tipos sin firmar (requiere “desperdicio” de al menos un bit de almacenamiento, y debe estar documentado), y es imposible para caracteres unsigned char . Si x tenía un tipo con signo, 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. Ver la respuesta de Jens Gustedt .

Los comstackdores intentan asignar registros a las variables, porque los registros son más rápidos que la memoria. Dado que el progtwig puede usar más variables de las que el procesador tiene registradas, los comstackdores realizan la asignación de registros, lo que conduce a diferentes variables usando el mismo registro en diferentes momentos. Considera el fragmento del progtwig

 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 no se ha inicializado aún, por lo tanto (por lo que el comstackdor) línea 3 debe ser algún tipo de casualidad que no puede suceder debido a otras condiciones que el comstackdor no fue lo suficientemente inteligente como para averiguarlo. Como z no se usa después de la línea 4, y x no se usa antes de la línea 5, se puede usar el mismo registro para ambas variables. Entonces, este pequeño progtwig se comstack para las siguientes operaciones en los registros:

 r1 = 0; r0 = 4; r0 = - r0; r1 += r0; r0 = r1; 

El valor final de x es el valor final de r0 , y el valor final de y es el valor final de r1 . Estos valores son x = -3 ey = -4, y no 5 y 4 como sucedería si x hubiera inicializado correctamente.

Para 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 comstackdor detecta que esa condition no tiene ningún efecto secundario. Como la condition no modifica x , el comstackdor sabe que la primera ejecución a través del bucle no puede tener acceso a x ya que aún no se ha inicializado. Por lo tanto, la primera ejecución del cuerpo del ciclo es equivalente a x = some_value() , no es necesario probar la condición. El comstackdor puede comstackr este código como si hubiera escrito

 unsigned 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 comstackdor es considerar que cualquier valor que dependa de x tiene el valor que sea conveniente siempre que x 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 comstackdor no necesita realizar un seguimiento de ninguna relación matemática especial entre los valores de lo que sea conveniente. Por lo tanto, el comstackdor puede analizar el código anterior de esta manera:

  • durante la primera iteración del bucle, x no está inicializado en el momento en que se evalúa x .
  • -x tiene un comportamiento indefinido, por lo que su valor es lo que sea conveniente.
  • La condition ? value : value regla de optimización condition ? value : value condition ? value : value aplica, por lo que este código se puede simplificar a condition ; value condition ; value .

Cuando se confronta con el código en su pregunta, este mismo comstackdor analiza que cuando se evalúa x = - x , el valor de -x es lo que sea-conveniente. Entonces la tarea puede ser optimizada.

No he buscado un ejemplo de un comstackdor que se comporte como se describió anteriormente, pero es el tipo de optimizaciones que los buenos comstackdores intentan hacer. No me sorprendería encontrar uno. Aquí hay un ejemplo menos plausible de un comstackdor con el que su progtwig falla. (Puede no ser tan inverosímil si comstack su progtwig en algún tipo de modo de depuración avanzada).

Este comstackdor hipotético mapea cada variable en una página de memoria diferente y configura los atributos de página para que la lectura de una variable no inicializada genere una trampa de procesador que invoca un depurador. Cualquier asignación a una variable primero se asegura de que su página de memoria esté asignada normalmente. Este comstackdor no intenta realizar ninguna optimización avanzada; está en modo de depuración, con la intención de localizar fácilmente errores tales como variables no inicializadas. Cuando se evalúa x = - x , el lado derecho causa una trampa y el depurador se activa.

Sí, el progtwig podría bloquearse. Podría haber, por ejemplo, representaciones de trampas (patrones de bits específicos que no se pueden manejar) que podrían causar una interrupción de CPU, que si no se maneja podría bloquear el progtwig.

(6.2.6.1 en un borrador final de C11 dice) Ciertas representaciones de objetos no necesitan representar un valor del tipo de objeto. Si el valor almacenado de un objeto tiene tal representación y es leído por una expresión lvalue que no tiene un tipo de carácter, el comportamiento no está definido. Si tal representación es producida por un efecto secundario que modifica todo o parte del objeto por una expresión lvalue que no tiene un tipo de carácter, el comportamiento no está definido.50) Tal representación se denomina representación de trampa.

(Esta explicación solo se aplica en plataformas donde unsigned int puede tener representaciones de trampas, lo cual es raro en sistemas del mundo real; vea comentarios para detalles y referencias a causas alternativas y quizás más comunes que conducen a la redacción actual del estándar.)

(Esta respuesta se dirige a C 1999. Para C 2011, ver 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 se inicialice 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ática 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 lo que nos preocupa son los objetos automáticos).

3.17.2 define “valor indeterminado” como “un valor no especificado o una representación de 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 ningún caso”.

Entonces, si la unsigned int x no unsigned int x tiene un valor no especificado, entonces x -= x debe producir cero. Eso deja la pregunta de si puede tratarse de una trampa. El acceso a un valor de trampa causa un comportamiento indefinido, según 6.2.6.1 5.

Algunos tipos de objetos pueden tener representaciones de trampa, como los NaN de señalización de números de coma 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 firmar no tiene bits de relleno, un int sin firmar no firmado no puede tener una representación de trap, y usar su valor no puede causar un comportamiento indefinido.

Sí, no está definido. El código puede bloquearse. 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 ventaja que todos los demás casos de comportamiento indefinido: el comstackdor no tiene que generar un código especial para que esto funcione.

Claramente, el comstackdor simplemente podría usar cualquier valor de basura que considere “útil” dentro de la variable, y funcionaría según lo previsto … ¿qué hay de malo con ese enfoque?

¿Por qué crees que eso no sucede? Ese es exactamente el enfoque adoptado. El comstackdor no está obligado a hacerlo funcionar, pero no es necesario para que falle.

Para cualquier variable de cualquier tipo, que no se inicialice o por otros motivos tenga un valor indeterminado, se aplica lo siguiente para el código que lee ese valor:

  • En el caso de que la variable tenga una duración de almacenamiento automática y no tenga su dirección tomada, el código siempre invoca un comportamiento indefinido [1].
  • De lo contrario, en caso de que el sistema admita representaciones de trampa para el tipo de variable dado, el código siempre invoca un comportamiento indefinido [2].
  • De lo contrario, si no hay representaciones de trampa, la variable toma un valor no especificado. No hay garantía de que este valor no especificado sea consistente cada vez que se lea la variable. Sin embargo, se garantiza que no se trata de 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 causar un locking del progtwig, aunque dicho código no es portátil para sistemas con representaciones de trampas.


[1]: C11 6.3.2.1:

Si lvalue designa un objeto de duración de almacenamiento automático que podría haberse declarado con la clase de almacenamiento de registros (nunca se tomó su dirección), y ese objeto no se inicializó (no se declaró con un inicializador y no se realizó ninguna asignación antes del uso ), el comportamiento no está definido.

[2]: C11 6.2.6.1:

Ciertas representaciones de objetos no necesitan representar un valor del tipo de objeto. Si el valor almacenado de un objeto tiene tal representación y es leído por una expresión lvalue que no tiene un tipo de carácter, el comportamiento no está definido. Si tal representación es producida por un efecto secundario que modifica todo o parte del objeto por una expresión lvalue que no tiene un tipo de carácter, el comportamiento no está definido.50) Tal representación se denomina representación de trampa.

[3] C11:

3.19.2
valor indeterminado
ya sea un valor no especificado o una representación de trampa

3.19.3
valor no especificado
valor válido del tipo relevante donde esta Norma Internacional no impone requisitos sobre qué valor se elige en cualquier instancia
NOTA Un valor no especificado no puede ser una representación de trampa.

3.19.4
representación de trampa
una representación de objeto que no necesita representar un valor del tipo de objeto

Si bien muchas respuestas se centran en los procesadores que atrapan el acceso de registro no inicializado, pueden surgir comportamientos extravagantes incluso en plataformas que no tienen tales trampas, utilizando comstackdores que no hacen ningún esfuerzo particular para explotar UB. Considera 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 comstackdor para una plataforma como ARM donde todas las instrucciones distintas de las cargas y las tiendas 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 produce un valor distinto de cero, r0 se cargará con un valor en el rango 0 … 65535. De lo contrario, cederá lo que tenía cuando se llamó a la función (es decir, el valor pasó 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 pueda producir dicho comportamiento invoca a UB.