Em esta resposta , Zwol fez esta afirmação:
A maneira correta de converter dois bytes de dados de uma fonte externa em um número inteiro assinado de 16 bits é com funções auxiliares como esta:
#include <stdint.h>
int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 8) |
(((uint32_t)data[1]) << 0);
return ((int32_t) val) - 0x10000u;
}
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 0) |
(((uint32_t)data[1]) << 8);
return ((int32_t) val) - 0x10000u;
}
Qual das funções acima é apropriada depende se a matriz contém uma pequena representação endian ou big endian. Endianness não é o problema em questão aqui, estou me perguntando por que o zwol subtrai 0x10000u
do uint32_t
valor convertido para int32_t
.
Por que essa é a maneira correta ?
Como evita o comportamento definido pela implementação ao converter para o tipo de retorno?
Como você pode assumir a representação do complemento de 2, como essa conversão mais simples falharia: return (uint16_t)val;
O que há de errado com esta solução ingênua:
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
c
casting
language-lawyer
chqrlie
fonte
fonte
int16_t
é definido pela implementação; portanto, a abordagem ingênua não é portátil.int16_t
0xFFFF0001u
não pode ser representado comoint16_t
e na segunda abordagem0xFFFFu
não pode ser representado comoint16_t
.Respostas:
Se
int
for de 16 bits, sua versão dependerá do comportamento definido pela implementação se o valor da expressão nareturn
instrução estiver fora do intervaloint16_t
.No entanto, a primeira versão também tem um problema semelhante; por exemplo, se
int32_t
for um typedef paraint
e os bytes de entrada forem ambos0xFF
, o resultado da subtração na instrução de retorno é oUINT_MAX
que causa um comportamento definido pela implementação quando convertido emint16_t
.IMHO, a resposta à qual você vincula tem vários problemas importantes.
fonte
int16_t
?uchar8_t
.Isso deve ser pedanticamente correto e funcionar também em plataformas que usam representações de complemento de bit de sinal ou 1 , em vez do complemento usual de 2 . Supõe-se que os bytes de entrada estejam no complemento de 2.
Por causa da filial, será mais caro que outras opções.
O que isso realiza é que evita qualquer suposição sobre como a
int
representação se relaciona com aunsigned
representação na plataforma. A conversão paraint
é necessária para preservar o valor aritmético de qualquer número que caiba no tipo de destino. Como a inversão garante que o bit superior do número de 16 bits seja zero, o valor será adequado. Então o unário-
e a subtração de 1 aplicam a regra usual para a negação do complemento de 2. Dependendo da plataforma,INT16_MIN
ainda poderá estourar se não couber noint
tipo no destino, caso em quelong
deve ser usado.A diferença para a versão original na pergunta ocorre no tempo de retorno. Enquanto o original sempre subtrai
0x10000
e o complemento do 2 permite que o excesso de sinal acondicionado oint16_t
alcance, esta versão tem o explícitoif
que evita o acúmulo de sinal (que é indefinido ).Agora, na prática, quase todas as plataformas em uso hoje usam a representação de complemento de 2. De fato, se a plataforma possui um padrão
stdint.h
que defineint32_t
, ela deve usar o complemento de 2 para isso. Onde essa abordagem às vezes é útil, é com algumas linguagens de script que não têm tipos de dados inteiros - você pode modificar as operações mostradas acima para flutuadores e isso dará o resultado correto.fonte
int16_t
e qualquer uma deintxx_t
suas variantes não assinadas deve usar a representação de complemento de 2 sem preenchimento de bits. Seria necessária uma arquitetura propositalmente perversa para hospedar esses tipos e usar outra representaçãoint
, mas acho que o DS9K poderia ser configurado dessa maneira.int
para evitar a confusão. De fato, se a plataforma defineint32_t
, deve ser o complemento de 2.intN_t
designa um tipo inteiro assinado com larguraN
, sem bits de preenchimento e uma representação complementar de dois. Assim,int8_t
denota um tipo inteiro assinado com uma largura de exatamente 8 bits. Outras representações ainda são suportadas pelo padrão, mas para outros tipos inteiros.(int)value
possui um comportamento definido por implementação se o tipoint
tiver apenas 16 bits. Receio que você precise usar(long)value - 0x10000
, mas nas arquiteturas de complemento que não são 2, o valor0x8000 - 0x10000
não pode ser representado como 16 bitsint
, portanto o problema permanece.long
funcionaria igualmente bem.Outro método - usando
union
:No programa:
first_byte
esecond_byte
pode ser trocado de acordo com o modelo endian pequeno ou grande. Este método não é melhor, mas é uma das alternativas.fonte
byte[2]
e comint16_t
o mesmo tamanho, é um ou outro dos dois pedidos possíveis, e não alguns valores arbitrários aleatórios de local em bits. Portanto, você pode pelo menos detectar, em tempo de compilação, qual endianness a implementação possui.Os operadores aritméticos mudam e bit a bit - ou na expressão
(uint16_t)data[0] | ((uint16_t)data[1] << 8)
não funcionam em tipos menores queint
, para que essesuint16_t
valores sejam promovidos paraint
(ouunsigned
sesizeof(uint16_t) == sizeof(int)
). Ainda assim, isso deve gerar a resposta correta, pois apenas os 2 bytes inferiores contêm o valor.Outra versão pedanticamente correta para a conversão de big-endian em little-endian (assumindo CPU little-endian) é:
memcpy
é usado para copiar a representaçãoint16_t
e essa é a maneira compatível com os padrões. Esta versão também compila em 1 instruçãomovbe
, consulte montagem .fonte
__builtin_bswap16
para a troca de bytes na ISO C não poder ser implementada com a mesma eficiência.int16_t
parauint16_t
está bem definida: os valores negativos são convertidos em valores maiores queINT_MAX
, mas a conversão desses valores nouint16_t
comportamento definido pela implementação: 6.3.1.3 Inteiros assinados e não assinados 1. Quando um valor com o tipo inteiro é convertido em outro tipo de número inteiro diferente de _Bool, se o valor pode ser representado pelo novo tipo, é inalterado. ... 3. Caso contrário, o novo tipo é assinado e o valor não pode ser representado nele; o resultado é definido pela implementação ou um sinal definido pela implementação é gerado.ntohs
/__builtin_bswap
e o|
/<<
pattern: gcc.godbolt.org/z/rJ-j87Aqui está outra versão que depende apenas de comportamentos portáteis e bem definidos (o cabeçalho
#include <endian.h>
não é padrão, o código é):A versão little-endian é compilada com uma
movbe
instrução únicaclang
, agcc
versão é menos ideal, consulte assembly .fonte
uint16_t
aint16_t
conversão, esta versão não tem essa conversão, então aqui está.Quero agradecer a todos os colaboradores por suas respostas. Aqui está o que o trabalho coletivo se resume a:
uint8_t
,int16_t
euint16_t
deve usar representação de complemento de dois, sem quaisquer bits de preenchimento, de modo que os bits reais da representação são inequivocamente os das 2 bytes na matriz, na ordem especificada pela os nomes das funções.(unsigned)data[0] | ((unsigned)data[1] << 8)
(para a versão little endian) é compilado em uma única instrução e gera um valor não assinado de 16 bits.uint16_t
em tipo assinadoint16_t
possui um comportamento definido pela implementação se o valor não estiver no intervalo do tipo de destino. Nenhuma provisão especial é feita para tipos cuja representação é definida com precisão.INT_MAX
e calcular o valor assinado correspondente subtraindo0x10000
. Fazer isso para todos os valores sugeridos por zwol pode produzir valores fora do intervaloint16_t
com o mesmo comportamento definido pela implementação.0x8000
bit explicitamente faz com que os compiladores produzam código ineficiente.memcpy
.Combinando os pontos 2 e 7, aqui está uma solução portátil e totalmente definida que compila eficientemente uma única instrução com o gcc e o clang :
Conjunto de 64 bits :
fonte
char
tipos podem alias ou conter a representação de objeto de qualquer outro tipo.uint16_t
não é um doschar
tipos, portanto,memcpy
deuint16_t
toint16_t
não é um comportamento bem definido. O padrão requer apenas que achar[sizeof(T)] -> T > char[sizeof(T)]
conversãomemcpy
seja bem definida.memcpy
deuint16_t
toint16_t
é definido na implementação, na melhor das hipóteses, não é portátil, não é bem definido, exatamente como a atribuição de um ao outro, e você não pode contorná-lo magicamentememcpy
. Não importa seuint16_t
usa ou não a representação de complemento de dois, ou se os bits de preenchimento estão presentes ou não - isso não é um comportamento definido ou exigido pelo padrão C.r = u
amemcpy(&r, &u, sizeof u)
mas o último não é melhor que o anterior, não é?