Eu rastreei um inseto extremamente desagradável escondido atrás desta pequena joia. Estou ciente de que, de acordo com a especificação C ++, os estouros assinados são um comportamento indefinido, mas somente quando o estouro ocorre quando o valor é estendido para a largura de bits sizeof(int)
. Pelo que entendi, incrementar um char
não deve ser um comportamento indefinido, contanto que sizeof(char) < sizeof(int)
. Mas isso não explica como c
obter um valor impossível . Como um número inteiro de 8 bits, como pode c
conter valores maiores do que sua largura 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;
}
Resultado
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 // <= ..........
Verifique no ideone.
c++
gcc
undefined-behavior
Sem sinal
fonte
fonte
printf()
ocorre a conversão?Respostas:
Este é um bug do compilador.
Embora a obtenção de resultados impossíveis para comportamento indefinido seja uma consequência válida, na verdade não há comportamento indefinido em seu código. O que está acontecendo é que o compilador pensa que o comportamento é indefinido e otimiza de acordo.
Se
c
for definido comoint8_t
eint8_t
promover aint
, entãoc--
deverá realizar a subtraçãoc - 1
emint
aritmética e converter o resultado de volta paraint8_t
. A subtração emint
não transborda e a conversão de valores integrais fora do intervalo em outro tipo integral é válida. Se o tipo de destino for assinado, o resultado será definido pela implementação, mas deve ser um valor válido para o tipo de destino. (E se o tipo de destino não tiver sinal, o resultado será bem definido, mas isso não se aplica aqui.)fonte
c
um tipo mais amplo. Presumivelmente, é o que está acontecendo aqui.Um compilador pode ter bugs que não estão em conformidade com o padrão, porque existem outros requisitos. Um compilador deve ser compatível com outras versões de si mesmo. Também pode ser esperado que seja compatível de algumas maneiras com outros compiladores, e também que esteja de acordo com algumas crenças sobre comportamento que são mantidas pela maioria de sua base de usuários.
Nesse caso, parece ser um bug de conformidade. A expressão
c--
deve ser manipulada dec
maneira semelhante ac = c - 1
. Aqui, o valor dec
à direita é promovido a tipoint
e, em seguida, ocorre a subtração. Comoc
está na faixa deint8_t
, essa subtração não irá estourar, mas pode produzir um valor que está fora da faixa deint8_t
. Quando esse valor é atribuído, ocorre uma conversão de volta ao tipoint8_t
para que o resultado se ajuste novamentec
. No caso fora do intervalo, a conversão tem um valor definido pela implementação. Mas um valor fora do intervalo deint8_t
não é um valor definido pela implementação válido. Uma implementação não pode "definir" que um tipo de 8 bits de repente contém 9 ou mais bits. Para o valor a ser definido pela implementação significa que algo na faixa deint8_t
é produzido e o programa continua. O padrão C, portanto, permite comportamentos como aritmética de saturação (comum em DSP's) ou wrap-around (arquiteturas convencionais).O compilador está usando um tipo de máquina subjacente mais amplo ao manipular valores de pequenos tipos inteiros como
int8_t
ouchar
. Quando a aritmética é realizada, os resultados que estão fora da faixa do tipo inteiro pequeno podem ser capturados com segurança neste tipo mais amplo. Para preservar o comportamento externamente visível de que a variável é do tipo de 8 bits, o resultado mais amplo deve ser truncado no intervalo de 8 bits. O código explícito é necessário para fazer isso, uma vez que os locais de armazenamento da máquina (registros) são maiores do que 8 bits e estão satisfeitos com os valores maiores. Aqui, o compilador negligenciou normalizar o valor e simplesmente o passouprintf
como está. O especificador de conversão%i
emprintf
não tem ideia de que o argumento veio originalmente deint8_t
cálculos; é apenas trabalhar com umint
argumento.fonte
Não consigo colocar isso em um comentário, então estou postando como uma resposta.
Por alguma razão muito estranha, o
--
operador é o culpado.Testei o código postado no Ideone e troquei
c--
porc = c - 1
e os valores permaneceram dentro do intervalo [-128 ... 127]:Olhos estranhos? Não sei muito sobre o que o compilador faz com expressões como
i++
oui--
. Provavelmente está promovendo o valor de retorno para umint
e passando-o. Essa é a única conclusão lógica que posso tirar porque você ESTÁ de fato obtendo valores que não cabem em 8 bits.fonte
c = c - 1
meiosc = (int8_t) ((int)c - 1
. A conversão de um fora do intervaloint
paraint8_t
tem um comportamento definido, mas um resultado definido pela implementação. Na verdade, nãoc--
deveria realizar essas mesmas conversões também?Eu acho que o hardware subjacente ainda está usando um registro de 32 bits para manter esse int8_t. Como a especificação não impõe um comportamento de estouro, a implementação não verifica o estouro e permite que valores maiores também sejam armazenados.
Se você marcar a variável local,
volatile
estará forçando o uso de memória para ela e, consequentemente, obter os valores esperados dentro do intervalo.fonte
printf
não se importar com ossizeof
valores do formato.O código assembler revela o problema:
EBX deve ser editado com FF pós-decremento, ou apenas BL deve ser usado com o restante de EBX transparente. Curioso que use sub em vez de dec. O -45 é totalmente misterioso. É a inversão bit a bit de 300 & 255 = 44. -45 = ~ 44. Existe uma conexão em algum lugar.
Ele passa por muito mais trabalho usando c = c - 1:
Em seguida, ele usa apenas a parte inferior do RAX, portanto, é restrito de -128 a 127. Opções do compilador "-g -O2".
Sem otimização, ele produz o código correto:
Portanto, é um bug no otimizador.
fonte
Use em
%hhd
vez de%i
! Deve resolver seu problema.O que você vê ali é o resultado de otimizações do compilador combinadas com você dizendo a printf para imprimir um número de 32 bits e depois colocando um número (supostamente de 8 bits) na pilha, que é realmente do tamanho de um ponteiro, porque é assim que o opcode push em x86 funciona.
fonte
g++ -O3
. Mudar%i
para%hhd
não muda nada.Acho que isso é feito por meio da otimização do código:
O compilador usa a
int32_t i
variável parai
ec
. Desative a otimização ou faça transmissão diretaprintf("c: %i\n", (int8_t)c--);
fonte
(int8_t)(c & 0x0000ffff)--
c
é ele próprio definido comoint8_t
, mas ao operar++
ou--
sobreint8_t
ele é implicitamente convertido primeiro emint
e o resultado da operação, em vez disso, o valor interno de c é impresso com printf, que por acaso éint
.Veja o valor real de
c
após todo o loop, especialmente após o último decrementoé o valor correto que se assemelha ao comportamento
-128 + 1 = 127
c
começa a usoint
de memória tamanho, mas impresso comoint8_t
quando impresso como a própria usando apenas8 bits
. Utiliza tudo32 bits
quando usado comoint
[Bug do compilador]
fonte
Acho que aconteceu porque o seu loop irá até o int i se tornar 300 ec se tornar -300. E o último valor é porque
fonte