En esta respuesta , zwol hizo esta afirmación:
La forma correcta de convertir dos bytes de datos de una fuente externa en un entero con signo de 16 bits es con funciones 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;
}
Cuál de las funciones anteriores es apropiada depende de si la matriz contiene una representación little endian o big endian. Endianness no es el problema en cuestión aquí, me pregunto por qué zwol resta 0x10000u
del uint32_t
valor convertido a int32_t
.
¿Por qué es esta la forma correcta ?
¿Cómo evita el comportamiento definido de implementación cuando se convierte al tipo de retorno?
Como puede asumir la representación del complemento de 2, ¿cómo podría fallar este elenco más simple? return (uint16_t)val;
Lo que está mal con esta solución ingenua:
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
fuente
fuente
int16_t
está definido por la implementación, por lo que el enfoque ingenuo no es portátil.int16_t
0xFFFF0001u
no se puede representar comoint16_t
, y en el segundo enfoque0xFFFFu
no se puede representar comoint16_t
.Respuestas:
Si
int
es de 16 bits, su versión se basa en el comportamiento definido por la implementación si el valor de la expresión en lareturn
declaración está fuera de rangoint16_t
.Sin embargo, la primera versión también tiene un problema similar; por ejemplo, si
int32_t
es un typedef paraint
, y los bytes de entrada son ambos0xFF
, entonces el resultado de la resta en la declaración de retorno es loUINT_MAX
que causa un comportamiento definido por la implementación cuando se convierte aint16_t
.En mi humilde opinión, la respuesta a la que se vincula tiene varios problemas importantes.
fuente
int16_t
?uchar8_t
.Esto debe ser pedagógicamente correcto y funcionar también en plataformas que usan bit de signo o representaciones de complemento de 1 , en lugar del complemento habitual de 2 . Se supone que los bytes de entrada están en el complemento de 2.
Debido a la sucursal, será más costoso que otras opciones.
Lo que esto logra es que evita cualquier suposición sobre cómo la
int
representación se relaciona con launsigned
representación en la plataforma. La conversión aint
se requiere para preservar el valor aritmético de cualquier número que se ajuste al tipo de destino. Debido a que la inversión asegura que el bit superior del número de 16 bits será cero, el valor se ajustará. Luego, el unario-
y la resta de 1 aplican la regla usual para la negación del complemento de 2. Dependiendo de la plataforma,INT16_MIN
aún podría desbordarse si no cabe en elint
tipo en el destino, en cuyo casolong
debe usarse.La diferencia con la versión original en la pregunta viene en el tiempo de regreso. Si bien el original siempre se resta
0x10000
y el complemento de 2 permite que el desbordamiento firmado lo envuelva alint16_t
rango, esta versión tiene el explícitoif
que evita el wrapover firmado (que no está definido ).Ahora en la práctica, casi todas las plataformas en uso hoy en día usan la representación del complemento 2. De hecho, si la plataforma cumple con el estándar
stdint.h
que defineint32_t
, debe usar el complemento de 2 para ello. Donde este enfoque a veces resulta útil es con algunos lenguajes de secuencias de comandos que no tienen tipos de datos enteros en absoluto: puede modificar las operaciones que se muestran arriba para los flotantes y dará el resultado correcto.fuente
int16_t
y cualquiera deintxx_t
sus variantes sin signo debe usar la representación del complemento 2 sin bits de relleno. Se necesitaría una arquitectura deliberadamente perversa para alojar estos tipos y usar otra representaciónint
, pero supongo que el DS9K podría configurarse de esta manera.int
para evitar la confusión. De hecho, si la plataforma defineint32_t
debe ser el complemento de 2.intN_t
designa un tipo entero con signo con anchoN
, sin bits de relleno y una representación complementaria de dos. Por lo tanto,int8_t
denota un tipo entero con signo con un ancho de exactamente 8 bits. El estándar todavía admite otras representaciones, pero para otros tipos enteros.(int)value
tiene un comportamiento definido de implementación si el tipoint
tiene solo 16 bits. Me temo que debe usarlo(long)value - 0x10000
, pero en arquitecturas de complemento que no son 2, el valor0x8000 - 0x10000
no puede representarse como 16 bitsint
, por lo que el problema persiste.long
funcionaría igualmente bien.Otro método: usar
union
:En programa:
first_byte
ysecond_byte
puede intercambiarse según el modelo endian pequeño o grande. Este método no es mejor pero es una de las alternativas.fuente
byte[2]
yint16_t
son del mismo tamaño, es uno o el otro de los dos ordenamientos posibles, no un arbitraria barajan valores de lugar a nivel de bit. Por lo tanto, al menos puede detectar en tiempo de compilación qué importancia tiene la implementación.Los operadores aritméticos shift y bit a bit o en expresión
(uint16_t)data[0] | ((uint16_t)data[1] << 8)
no funcionan en tipos más pequeñosint
, de modo que esosuint16_t
valores se promueven aint
(ounsigned
sisizeof(uint16_t) == sizeof(int)
). Sin embargo, eso debería dar la respuesta correcta, ya que solo los 2 bytes inferiores contienen el valor.Otra versión pedagógicamente correcta para la conversión big-endian a little-endian (suponiendo CPU little-endian) es:
memcpy
se utiliza para copiar la representación deint16_t
y esa es la forma de hacerlo que cumple con los estándares. Esta versión también se compila en 1 instrucciónmovbe
, ver ensamblaje .fuente
__builtin_bswap16
existe porque el intercambio de bytes en ISO C no se puede implementar de manera tan eficiente.int16_t
auint16_t
está bien definida: los valores negativos se convierten en valores mayores queINT_MAX
, pero volver a convertir estos valoresuint16_t
es un comportamiento definido por la implementación: 6.3.1.3 Enteros con y sin signo 1. Cuando un valor con tipo de entero se convierte en otro tipo de entero distinto de Bool, si el valor puede ser representado por el nuevo tipo, no cambia. ... 3. De lo contrario, el nuevo tipo está firmado y el valor no se puede representar en él; el resultado está definido por la implementación o se genera una señal definida por la implementación.ntohs
/__builtin_bswap
y el|
/<<
patrón: gcc.godbolt.org/z/rJ-j87Aquí hay otra versión que se basa solo en comportamientos portátiles y bien definidos (el encabezado
#include <endian.h>
no es estándar, el código sí lo es):La versión little-endian se compila con una sola
movbe
instrucciónclang
, lagcc
versión es menos óptima, vea el ensamblaje .fuente
uint16_t
aint16_t
la conversión, esta versión no tiene esa conversión, así que aquí tienes.Quiero agradecer a todos los contribuyentes por sus respuestas. Esto es a lo que se reduce el trabajo colectivo:
uint8_t
,int16_t
yuint16_t
deben usar la representación del complemento a dos sin ningún bit de relleno, por lo que los bits reales de la representación son inequívocamente los de los 2 bytes en la matriz, en el orden especificado por Los nombres de las funciones.(unsigned)data[0] | ((unsigned)data[1] << 8)
(para la versión little endian) se compila en una sola instrucción y produce un valor de 16 bits sin signo.uint16_t
a tipo con signoint16_t
tiene un comportamiento definido de implementación si el valor no está en el rango del tipo de destino. No se hacen disposiciones especiales para los tipos cuya representación se define con precisión.INT_MAX
y calcular el valor firmado correspondiente restando0x10000
. Hacer esto para todos los valores sugeridos por zwol puede producir valores fuera del rango deint16_t
con el mismo comportamiento definido de implementación.0x8000
bit explícitamente hace que los compiladores produzcan código ineficiente.memcpy
.Combinando los puntos 2 y 7, aquí hay una solución portátil y totalmente definida que se compila eficientemente en una sola instrucción con gcc y clang :
Asamblea de 64 bits :
fuente
char
tipos pueden tener alias o contener la representación de objetos de cualquier otro tipo.uint16_t
No es uno dechar
los tipos, para quememcpy
deuint16_t
aint16_t
no es un comportamiento bien definido. El estándar solo requierechar[sizeof(T)] -> T > char[sizeof(T)]
conversión conmemcpy
estar bien definido.memcpy
deuint16_t
queint16_t
es definido por la implementación en el mejor, no es portátil, no bien definida, exactamente como la asignación de uno a otro, y no se puede eludir que mágicamente conmemcpy
. No importa siuint16_t
usa la representación del complemento de dos o no, o si los bits de relleno están presentes o no, ese no es un comportamiento definido o requerido por el estándar C.r = u
amemcpy(&r, &u, sizeof u)
pero éste no es mejor que el anterior, ¿verdad?