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 0x10000udel uint32_tvalor 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_testá definido por la implementación, por lo que el enfoque ingenuo no es portátil.int16_t0xFFFF0001uno se puede representar comoint16_t, y en el segundo enfoque0xFFFFuno se puede representar comoint16_t.Respuestas:
Si
intes de 16 bits, su versión se basa en el comportamiento definido por la implementación si el valor de la expresión en lareturndeclaración está fuera de rangoint16_t.Sin embargo, la primera versión también tiene un problema similar; por ejemplo, si
int32_tes un typedef paraint, y los bytes de entrada son ambos0xFF, entonces el resultado de la resta en la declaración de retorno es loUINT_MAXque 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
intrepresentación se relaciona con launsignedrepresentación en la plataforma. La conversión aintse 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_MINaún podría desbordarse si no cabe en elinttipo en el destino, en cuyo casolongdebe usarse.La diferencia con la versión original en la pregunta viene en el tiempo de regreso. Si bien el original siempre se resta
0x10000y el complemento de 2 permite que el desbordamiento firmado lo envuelva alint16_trango, esta versión tiene el explícitoifque 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.hque 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_ty cualquiera deintxx_tsus 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.intpara evitar la confusión. De hecho, si la plataforma defineint32_tdebe ser el complemento de 2.intN_tdesigna un tipo entero con signo con anchoN, sin bits de relleno y una representación complementaria de dos. Por lo tanto,int8_tdenota 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)valuetiene un comportamiento definido de implementación si el tipointtiene solo 16 bits. Me temo que debe usarlo(long)value - 0x10000, pero en arquitecturas de complemento que no son 2, el valor0x8000 - 0x10000no puede representarse como 16 bitsint, por lo que el problema persiste.longfuncionaría igualmente bien.Otro método: usar
union:En programa:
first_byteysecond_bytepuede 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_tson 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_tvalores se promueven aint(ounsignedsisizeof(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:
memcpyse utiliza para copiar la representación deint16_ty 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_bswap16existe porque el intercambio de bytes en ISO C no se puede implementar de manera tan eficiente.int16_tauint16_testá bien definida: los valores negativos se convierten en valores mayores queINT_MAX, pero volver a convertir estos valoresuint16_tes 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_bswapy 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
movbeinstrucciónclang, lagccversión es menos óptima, vea el ensamblaje .fuente
uint16_taint16_tla 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_tyuint16_tdeben 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_ta tipo con signoint16_ttiene 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_MAXy calcular el valor firmado correspondiente restando0x10000. Hacer esto para todos los valores sugeridos por zwol puede producir valores fuera del rango deint16_tcon el mismo comportamiento definido de implementación.0x8000bit 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
chartipos pueden tener alias o contener la representación de objetos de cualquier otro tipo.uint16_tNo es uno decharlos tipos, para quememcpydeuint16_taint16_tno es un comportamiento bien definido. El estándar solo requierechar[sizeof(T)] -> T > char[sizeof(T)]conversión conmemcpyestar bien definido.memcpydeuint16_tqueint16_tes 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_tusa 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 = uamemcpy(&r, &u, sizeof u)pero éste no es mejor que el anterior, ¿verdad?