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

82

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 xdeberí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?

usuario541686
fuente
3
¿Cuál es la ventaja dada al definir aquí un caso especial para el comportamiento? Claro, hagamos que nuestros programas y bibliotecas sean más grandes y lentos porque @Mehrdad quiere evitar inicializar una variable en un caso específico y poco común.
Paul Tomblin
9
@ W'rkncacnter No estoy de acuerdo con que sea un engaño. Independientemente del valor que tome, el OP espera que sea cero después x -= x. La pregunta es por qué acceder a valores no inicializados es UB.
Mysticial
6
Es interesante que la declaración x = 0; normalmente se convierte a xor x, x en ensamblado. Es casi lo mismo que estás intentando hacer aquí, pero con xor en lugar de restar.
0xFE
1
'es decir, ¿cuál es la ventaja dada por no definir el comportamiento aquí? '- Hubiera pensado que la ventaja del estándar no enumerar la infinidad de expresiones con valores que no dependen de una o más variables para ser obvia. Al mismo tiempo, @Paul, tal cambio en el estándar no haría que los programas y las bibliotecas fueran más grandes.
Jim Balter

Respuestas:

90

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:

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

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;
  • Aquí se toman las direcciones de ay b, por lo que su valor es simplemente indeterminado.
  • Dado que unsigned charnunca ha habido representaciones de trampa en las que el valor indeterminado no está especificado, cualquier valor de unsigned charpodría suceder.
  • Al final a debe contener el valor 0.

Edit2: a y btienen 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 caso

Jens Gustedt
fuente
6
Quizás me estoy perdiendo algo, pero me parece que unsignedseguro 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 ".
conio
6
Continuando con el comentario: "Algunas combinaciones de bits de relleno pueden generar representaciones de trampas, por ejemplo, si un bit de relleno es un bit de paridad. Independientemente, ninguna operación aritmética en valores válidos puede generar una representación de trampa que no sea como parte de una condición excepcional como un desbordamiento, y esto no puede ocurrir con tipos sin firmar ". - Eso es genial una vez que tenemos un valor válido con el que trabajar, pero el valor indeterminado puede ser una representación de trampa antes de ser inicializado (por ejemplo, bit de paridad configurado incorrectamente).
conio
4
@conio Tiene razón para todos los tipos excepto unsigned char, pero esta respuesta está usando unsigned char. Sin embargo, tenga en cuenta: un programa estrictamente conforme puede calcular sizeof(unsigned) * CHAR_BITy determinar, basándose en UINT_MAX, que implementaciones particulares no pueden tener representaciones de trampas para unsigned. Una vez que ese programa ha tomado esa determinación, puede proceder a hacer exactamente lo que hace esta respuesta unsigned char.
4
@JensGustedt: ¿No es memcpyuna distracción, es decir, no se aplicaría su ejemplo si fuera reemplazado por *&a = *&b;.
R .. GitHub DEJA DE AYUDAR A ICE
4
@R .. Ya no estoy seguro. Hay una discusión en curso sobre la lista de correo del comité C, y parece que todo esto es un gran lío, es decir, una gran brecha entre lo que es (o ha sido) el comportamiento previsto y lo que realmente está escrito. Sin embargo, lo que está claro es que acceder a la memoria a medida unsigned charque memcpyayuda y, por lo tanto , ayuda, el de *&es menos claro. Informaré una vez que esto se calme.
Jens Gustedt
24

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 xnunca se toma su dirección, por lo que es "similar a un registro". También serían válidos si el tipo de xtrampa 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 para unsigned char. Si xtuviera 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, xaú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. Dado zque no se usa después de la línea 4 y xno 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 xes el valor final de r0y el valor final de yes el valor final de r1. Estos valores son x = -3 e y = -4, y no 5 y 4 como sucedería si se xhubiera 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 conditionno tiene efectos secundarios. Dado conditionque no modifica x, el compilador sabe que la primera ejecución a través del bucle no puede tener acceso xya que aún no está inicializado. Por lo tanto, la primera ejecución del cuerpo del bucle es equivalente a x = some_value(), no es necesario probar la condición. El compilador puede compilar 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 compilador es considerar que cualquier valor que dependa de xtiene el valor que sea conveniente siempre que xno 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:

  • durante la primera iteración del ciclo, xno se inicializa cuando -xse evalúa el tiempo .
  • -x tiene un comportamiento indefinido, por lo que su valor es el que sea conveniente.
  • Se aplica la regla de optimización , por lo que este código se puede simplificar a .condition ? value : valuecondition; value

Cuando se enfrenta al código en su pregunta, este mismo compilador analiza que cuando x = - xse evalúa, el valor de -xes 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 = - xse evalúa, el lado derecho provoca una trampa y el depurador se activa.

Gilles 'SO- deja de ser malvado'
fuente
+1 Bonita explicación, el estándar está cuidando especialmente esa situación. Para una continuación de esa historia, vea mi respuesta a continuación. (demasiado tiempo para tener un comentario).
Jens Gustedt
@JensGustedt Oh, su respuesta hace un punto muy importante que yo (y otros) pasamos por alto: a menos que el tipo tenga valores de trampa, que para un tipo sin firmar requiere "desperdiciar" al menos un bit, xtiene un valor no inicializado pero el comportamiento al acceder sería definirse si x no tuvo un comportamiento similar al de un registro.
Gilles 'SO- deja de ser malvado'
@Gilles: al menos clang hace el tipo de optimizaciones que mencionaste: (1) , (2) , (3) .
Vlad
1
¿Qué ventaja práctica hay en que clang procese las cosas de esa manera? Si el código descendente nunca usa el valor de 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 ejemplo if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;, estaría igualmente satisfecho con cualquier valor 0-255 que xpudiera contener en el caso de que volatile1hubiera arrojado cero, creo que una implementación que permitiría al programador omitir una escritura innecesaria xdebería considerarse de mayor calidad que una que se comportaría ...
supercat
... de una manera totalmente impredecible en ese caso. Una implementación que generaría de manera confiable una trampa definida por la implementación en ese caso podría, para ciertos propósitos, considerarse de mayor calidad todavía, pero comportarse de manera totalmente impredecible me parece la forma de comportamiento de menor calidad para casi cualquier propósito.
supercat
16

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.

(6.2.6.1 en un borrador C11 tardío 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 tipo de carácter, el comportamiento es indefinido. 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 tipo de carácter, el comportamiento es indefinido.50) Tal representación se llama representación trampa.

(Esta explicación solo se aplica en plataformas donde unsigned intpueden 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).

eq-
fuente
3
@VladLazarenko: Se trata de C, no de CPU en particular. Cualquiera puede diseñar trivialmente una CPU que tenga patrones de bits para números enteros que la vuelvan loca. Considere una CPU que tiene un "bit loco" en sus registros.
David Schwartz
2
Entonces, ¿puedo decir, entonces, que el comportamiento está bien definido en el caso de enteros y x86?
3
Bueno, en teoría, podría tener un compilador que decidiera usar solo enteros de 28 bits (en x86) y agregar un código específico para manejar cada suma, multiplicación (etc.) y asegurarse de que estos 4 bits no se usen (o emitan un SIGSEGV de lo contrario) ). Un valor no inicializado podría causar esto.
eq-
4
Odio cuando alguien insulta a los demás porque ese alguien no entiende el problema. Si el comportamiento es indefinido es completamente una cuestión de lo que dice el estándar. Ah, y no hay nada práctico en el escenario de eq ... es completamente artificial.
Jim Balter
7
@Vlad Lazarenko: Las CPU Itanium tienen un indicador NaT (Not a Thing) para cada registro de enteros. El indicador NaT se utiliza para controlar la ejecución especulativa y puede permanecer en registros que no se inicializan correctamente antes de su uso. La lectura de un registro de este tipo con un conjunto de bits NaT produce una excepción. Ver blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx
Mainframe nórdico
13

(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 xtiene un valor no especificado, entonces x -= xdebe 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.

Eric Postpischil
fuente
Si xtiene una representación de trampa, entonces x -= xpodrí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.
user541686
Sí, si el tipo de xtiene una representación de trampa, entonces x -= xpodría trampa. Incluso si se xusa simplemente como valor podría atrapar. (Es seguro usarlo xcomo un valor l; la escritura en un objeto no se verá afectada por una representación de trampa que esté en él).
Eric Postpischil
los tipos sin firmar rara vez tienen una representación de trampa
Jens Gustedt
Citando a Raymond Chen , "En el ia64, cada registro de 64 bits es en realidad 65 bits. El bit adicional se llama" NaT ", que significa" nada ". El bit se establece cuando el registro no contiene un valor válido. Piense en ello como la versión entera del punto flotante NaN. ... si tiene un registro cuyo valor es NaT y lo respira de manera incorrecta (por ejemplo, intente guardar su valor en la memoria), el el procesador generará una excepción STATUS_REG_NAT_CONSUMPTION ". Es decir, un bit trap puede estar completamente fuera del valor.
Saludos y hth. - Alf
−1 La declaración "Si, en su plataforma de destino, un int sin signo no tiene bits de relleno, entonces un int sin signo no inicializado no puede tener una representación de trampa, y usar su valor no puede causar un comportamiento indefinido". no considera esquemas como los bits x64 NaT.
Saludos y hth. - Alf
11

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.

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?

¿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.

David Schwartz
fuente
1
Sin embargo, el compilador tampoco tiene que tener un código especial para esto. Simplemente asignar el espacio (como siempre) y no inicializar la variable le da el comportamiento correcto. No creo que eso necesite una lógica especial.
user541686
7
1) Claro, podrían haberlo hecho. Pero no puedo pensar en ningún argumento que lo mejore. 2) La plataforma sabe que no se puede confiar en el valor de la memoria no inicializada, por lo que es libre de cambiarlo. Por ejemplo, puede poner a cero la memoria no inicializada en segundo plano para tener páginas puestas a cero listas para su uso cuando sea necesario. (Considere si esto sucede: 1) Leemos el valor a restar, digamos que obtenemos 3. 2) La página se pone a cero porque no está inicializada, cambiando el valor a 0. 3) Hacemos una resta atómica, asignando la página y haciendo el valor -3. Ups.)
David Schwartz
2
-1 porque no da ninguna justificación para su afirmación. Hay situaciones en las que sería válido esperar que el compilador simplemente tome el valor que está escrito en la ubicación de la memoria.
Jens Gustedt
1
@JensGustedt: No entiendo tu comentario. ¿Puedes por favor aclarar?
David Schwartz
3
Porque simplemente afirmas que hay una regla general, sin hacer referencia a ella. Como tal, es solo un intento de "prueba por autoridad" que no es lo que espero de SO. Y por no argumentar efectivamente por qué esto no podía ser un valor inespecífico. La única razón por la que se trata de UB en el caso general es que xpodría declararse como register, 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.
Jens Gustedt
6

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:

  • En caso de que la variable tenga una duración de almacenamiento automática y no se tome su dirección, el código siempre invoca un comportamiento indefinido [1].
  • De lo contrario, en caso de que el sistema admita representaciones de trampas para el tipo de variable dado, el código siempre invoca un comportamiento indefinido [2].
  • 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:

Si lvalue designa un objeto de duración de almacenamiento automático que podría haberse declarado con la clase de almacenamiento de registro (nunca se tomó su dirección), y ese objeto no está inicializado (no declarado con un inicializador y no se ha realizado ninguna asignación antes de su 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 tipo de carácter, el comportamiento es indefinido. 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 tipo de carácter, el comportamiento es indefinido.50) Tal representación se llama representación 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 caso
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

Lundin
fuente
Yo diría que esto se resuelve en "Siempre es un comportamiento indefinido", ya que la máquina abstracta de C, puede tener representaciones de trampa. El hecho de que su implementación no los use no hace que el código esté definido. De hecho, una lectura estricta ni siquiera insistiría en que las representaciones de trampas tienen que estar en hardware por lo que no puedo decir. No veo por qué un compilador no puede decidir que un patrón de bits específico es una trampa, verifíquelo cada vez que se lea la variable. e invocar UB.
Vality
2
@Vality En el mundo real, el 99,9999% de todas las computadoras son CPU de complemento a dos sin representaciones de trampas. Por lo tanto, ninguna representación de trampas es la norma y discutir el comportamiento en tales computadoras del mundo real es muy relevante. Asumir que las computadoras exóticas son la norma no es útil. Las representaciones de trampas en el mundo real son tan raras que la presencia del término representación de trampas en el estándar debe considerarse principalmente como un defecto estándar heredado de la década de 1980. Como es el soporte para las computadoras de complemento y de signo y magnitud.
Lundin
2
Por cierto, esta es una excelente razón por la stdint.hque siempre debe usarse en lugar de los tipos nativos de C. Porque stdint.haplica el complemento a 2 y no los bits de relleno. En otras palabras, los stdint.htipos no pueden estar llenos de basura.
Lundin
2
Una vez más, la respuesta del comité al informe de defectos dice que: "La respuesta a la pregunta 2 es que cualquier operación realizada en valores indeterminados tendrá como resultado un valor indeterminado". y "La respuesta a la pregunta 3 es que las funciones de la biblioteca exhibirán un comportamiento indefinido cuando se utilicen en valores indeterminados".
Antti Haapala
2
DRs 451 y 260
Antti Haapala
2

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.

Super gato
fuente
Interesante. Entonces, ¿estás diciendo que la respuesta aceptada es incorrecta? ¿O está diciendo que es correcto en teoría pero que en la práctica los compiladores pueden hacer cosas más raras?
user541686
@Mehrdad: Es común que las implementaciones tengan un comportamiento que va más allá de los límites de lo que sería posible en ausencia de UB. Creo que sería útil que el Estándar reconociera el concepto de un valor parcialmente indeterminado cuyos bits "asignados" se comportarán de una manera que es, en el peor de los casos, no especificada, pero con bits superiores adicionales que se comportan de manera no determinista (por ejemplo, si el el resultado de la función anterior se almacena en una variable de tipo uint16_t, esa variable a veces puede leerse como 123 y, a veces, como 6553623). Si el resultado termina siendo ignorado ...
supercat
... o usado de tal manera que cualquier forma posible de leerlo produzca resultados finales que cumplan con los requisitos, la existencia de un valor parcialmente indeterminado no debería ser un problema. Por otro lado, no hay nada en la Norma que permita la existencia de valores parcialmente indeterminados en cualquier circunstancia en la que la Norma imponga algún requisito de comportamiento.
supercat
Me parece que lo que está describiendo es exactamente lo que está en la respuesta aceptada: que si una variable podría haberse declarado con register, entonces puede tener bits adicionales que hacen que el comportamiento sea potencialmente indefinido. Eso es exactamente lo que estás diciendo, ¿verdad?
user541686
@Mehrdad: La respuesta aceptada se centra en arquitecturas cuyos registros tienen un estado extra "no inicializado" y se interceptan si se carga un registro no inicializado. Estas arquitecturas existen, pero no son comunes. Describo un escenario en el que el hardware común puede exhibir un comportamiento que está fuera del ámbito de cualquier cosa contemplada por el Estándar C, pero sería útilmente limitado si un compilador no agrega su propia extravagancia adicional a la mezcla. Por ejemplo, si una función tiene un parámetro que selecciona una operación para realizar, y algunas operaciones devuelven datos útiles pero otras no, ...
supercat