Localicé un error extremadamente desagradable que se escondía detrás de esta pequeña joya. Soy consciente de que según la especificación de C ++, los desbordamientos firmados son un comportamiento indefinido, pero solo cuando el desbordamiento se produce cuando el valor se extiende al ancho de bits sizeof(int)
. Según tengo entendido, aumentar un char
nunca debería ser un comportamiento indefinido siempre que sizeof(char) < sizeof(int)
. Pero eso no explica cómo c
se obtiene un valor imposible . Como un entero de 8 bits, ¿cómo pueden c
contener valores mayores que su ancho de bits?
Código
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
Salida
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
Compruébalo en ideone.
c++
gcc
undefined-behavior
No firmado
fuente
fuente
printf()
funciona la conversión?Respuestas:
Este es un error del compilador.
Aunque obtener resultados imposibles para un comportamiento indefinido es una consecuencia válida, en realidad no hay un comportamiento indefinido en su código. Lo que sucede es que el compilador cree que el comportamiento no está definido y lo optimiza en consecuencia.
Si
c
se define comoint8_t
, yint8_t
asciende aint
,c--
se supone que realiza la restac - 1
enint
aritmética y convierte el resultado enint8_t
. La resta enint
no se desborda, y la conversión de valores integrales fuera de rango a otro tipo integral es válida. Si el tipo de destino está firmado, el resultado está definido por la implementación, pero debe ser un valor válido para el tipo de destino. (Y si el tipo de destino no está firmado, el resultado está bien definido, pero eso no se aplica aquí).fuente
c
en un tipo más amplio. Presumiblemente, eso es lo que está sucediendo aquí.Un compilador puede tener errores distintos a las no conformidades con el estándar, porque existen otros requisitos. Un compilador debe ser compatible con otras versiones de sí mismo. También se puede esperar que sea compatible de alguna manera con otros compiladores, y también que se ajuste a algunas creencias sobre el comportamiento sostenidas por la mayoría de su base de usuarios.
En este caso, parece ser un error de conformidad. La expresión
c--
debe manipularsec
de forma similar ac = c - 1
. Aquí, el valor dec
a la derecha se promueve a tipoint
, y luego se lleva a cabo la resta. Comoc
está en el rango deint8_t
, esta resta no se desbordará, pero puede producir un valor que esté fuera del rango deint8_t
. Cuando se asigna este valor, se vuelve a realizar una conversión al tipoint8_t
para que el resultado vuelva a encajarc
. En el caso de fuera de rango, la conversión tiene un valor definido por la implementación. Pero un valor fuera del rango deint8_t
no es un valor definido por la implementación válido. Una implementación no puede "definir" que un tipo de 8 bits contenga repentinamente 9 o más bits. Que el valor esté definido por la implementación significa queint8_t
se produce algo en el rango de y el programa continúa. Por lo tanto, el estándar C permite comportamientos como la aritmética de saturación (común en los DSP) o el envolvente (arquitecturas convencionales).El compilador está utilizando un tipo de máquina subyacente más amplio al manipular valores de tipos enteros pequeños como
int8_t
ochar
. Cuando se realiza aritmética, los resultados que están fuera del rango del tipo de entero pequeño se pueden capturar de manera confiable en este tipo más amplio. Para preservar el comportamiento visible externamente de que la variable es un tipo de 8 bits, el resultado más amplio tiene que truncarse en el rango de 8 bits. Se requiere código explícito para hacer eso, ya que las ubicaciones de almacenamiento de la máquina (registros) son más anchas que 8 bits y están contentas con los valores más grandes. Aquí, el compilador se olvidó de normalizar el valor y simplemente se lo pasó tal cualprintf
. El especificador de conversión%i
enprintf
no tiene idea de que el argumento proviene originalmente deint8_t
cálculos; solo está trabajando con unint
argumento.fuente
No puedo incluir esto en un comentario, así que lo publico como respuesta.
Por alguna extraña razón,
--
resulta que el operador es el culpable.Probé el código publicado en Ideone y lo reemplacé
c--
conc = c - 1
y los valores permanecieron dentro del rango [-128 ... 127]:Freaky ey? No sé mucho sobre lo que hace el compilador con expresiones como
i++
oi--
. Es probable que promueva el valor de retorno de anint
y lo pase. Esa es la única conclusión lógica a la que puedo llegar porque, de hecho, ESTÁS obteniendo valores que no pueden caber en 8 bits.fuente
c = c - 1
mediosc = (int8_t) ((int)c - 1
. La conversión de un fuera de rangoint
enint8_t
tiene un comportamiento definido pero un resultado definido por la implementación. En realidad, ¿noc--
se supone que también debe realizar esas mismas conversiones?Supongo que el hardware subyacente todavía usa un registro de 32 bits para mantener ese int8_t. Dado que la especificación no impone un comportamiento de desbordamiento, la implementación no comprueba el desbordamiento y también permite almacenar valores más grandes.
Si marca la variable local ya
volatile
que está forzando a usar memoria para ella y consecuentemente obtiene los valores esperados dentro del rango.fuente
printf
no preocuparse por lossizeof
valores de formato.El código ensamblador revela el problema:
EBX debe agregarse con la disminución posterior de FF, o solo debe usarse BL con el resto de EBX limpio. Curioso que use sub en lugar de dec. El -45 es absolutamente misterioso. Es la inversión bit a bit de 300 y 255 = 44. -45 = ~ 44. Hay una conexión en alguna parte.
Pasa por mucho más trabajo usando c = c - 1:
Luego usa solo la porción baja de RAX, por lo que está restringido a -128 a 127. Opciones del compilador "-g -O2".
Sin optimización, produce el código correcto:
Entonces es un error en el optimizador.
fuente
Use en
%hhd
lugar de%i
! Debería resolver tu problema.Lo que ve allí es el resultado de las optimizaciones del compilador combinadas con usted diciéndole a printf que imprima un número de 32 bits y luego presionando un número (supuestamente de 8 bits) en la pila, que en realidad tiene el tamaño de un puntero, porque así es como funciona el código de operación push en x86.
fuente
g++ -O3
. Cambiar%i
a%hhd
no cambia nada.Creo que esto se hace mediante la optimización del código:
El compilador usa la
int32_t i
variable parai
yc
. Desactive la optimización o realice un envío directoprintf("c: %i\n", (int8_t)c--);
fuente
(int8_t)(c & 0x0000ffff)--
c
se define en sí mismo comoint8_t
, pero cuando opera++
o--
sobreint8_t
, se convierte implícitamente primero enint
y el resultado de la operación en su lugar, el valor interno de c se imprime con printf que resulta serint
.Ver el valor real de
c
después del ciclo completo, especialmente después del último decrementoes el valor correcto que se asemeja al comportamiento
-128 + 1 = 127
c
comienza a usar laint
memoria de tamaño, pero se imprime comoint8_t
cuando se imprime como él mismo usando solamente8 bits
. Utiliza todo32 bits
cuando se usa comoint
[Error del compilador]
fuente
Creo que sucedió porque su ciclo continuará hasta que el int i se convierta en 300 yc en -300. Y el último valor es porque
fuente