Dans cette réponse , zwol a fait cette affirmation:
La bonne façon de convertir deux octets de données d'une source externe en un entier signé 16 bits est avec des fonctions d'assistance comme celle-ci:
#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;
}
Laquelle des fonctions ci-dessus est appropriée dépend du fait que le tableau contient un petit endian ou une grande représentation endian. L'endianité n'est pas le problème en cause ici, je me demande pourquoi zwol soustrait 0x10000u
de la uint32_t
valeur convertie en int32_t
.
Pourquoi est-ce la bonne façon ?
Comment évite-t-il le comportement défini par l'implémentation lors de la conversion au type de retour?
Puisque vous pouvez assumer la représentation du complément à 2, comment cette conversion plus simple échouerait-elle: return (uint16_t)val;
Quel est le problème avec cette solution naïve:
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
la source
int16_t
est défini par l'implémentation, de sorte que l'approche naïve n'est pas portable.int16_t
0xFFFF0001u
ne peut pas être représenté commeint16_t
, et dans la deuxième approche0xFFFFu
ne peut pas être représenté commeint16_t
.Réponses:
Si
int
est 16 bits, votre version s'appuie sur un comportement défini par l'implémentation si la valeur de l'expression dans l'return
instruction est hors limites pourint16_t
.Cependant, la première version a également un problème similaire; par exemple, si
int32_t
est un typedef pourint
, et que les octets d'entrée sont les deux0xFF
, le résultat de la soustraction dans l'instruction de retour estUINT_MAX
ce qui provoque le comportement défini par l'implémentation lors de la conversion enint16_t
.À mon humble avis, la réponse à laquelle vous liez a plusieurs problèmes majeurs.
la source
int16_t
?uchar8_t
.Cela devrait être correct sur le plan pédiatrique et fonctionner également sur les plates-formes qui utilisent le bit de signe ou les représentations du complément à 1 , au lieu du complément à 2 habituel . Les octets d'entrée sont supposés être en complément de 2.
En raison de la succursale, ce sera plus cher que les autres options.
Cela permet d'éviter toute hypothèse sur la
int
relation entre launsigned
représentation et la représentation sur la plate-forme. Le transtypage enint
est requis pour conserver la valeur arithmétique de tout nombre pouvant tenir dans le type cible. Étant donné que l'inversion garantit que le bit supérieur du nombre 16 bits sera nul, la valeur s'adaptera. Ensuite, l'unaire-
et la soustraction de 1 appliquent la règle habituelle pour la négation du complément à 2. Selon la plate-forme, elleINT16_MIN
pourrait toujours déborder si elle ne correspond pas auint
type sur la cible, auquel cas ellelong
doit être utilisée.La différence avec la version originale dans la question vient au moment du retour. Alors que l'original est toujours soustrait
0x10000
et que le complément à 2 laisse le débordement signé l'envelopperint16_t
, il est expliciteif
qui évite le wrapover signé (qui n'est pas défini ).Maintenant, dans la pratique, presque toutes les plateformes utilisées aujourd'hui utilisent la représentation du complément à 2. En fait, si la plate-forme est conforme aux normes
stdint.h
qui la définitint32_t
, elle doit utiliser le complément 2 pour cela. Lorsque cette approche est parfois utile, c'est avec certains langages de script qui n'ont pas du tout de types de données entiers - vous pouvez modifier les opérations indiquées ci-dessus pour les flottants et cela donnera le résultat correct.la source
int16_t
touteintxx_t
variante non signée doit utiliser la représentation du complément à 2 sans bits de remplissage. Il faudrait une architecture délibérément perverse pour héberger ces types et utiliser une autre représentationint
, mais je suppose que le DS9K pourrait être configuré de cette façon.int
pour éviter la confusion. En effet, si la plateforme le définit,int32_t
il doit s'agir du complément à 2.intN_t
désigne un type entier signé avec largeurN
, sans bits de remplissage et une représentation du complément à deux. Ainsi,int8_t
désigne un type entier signé avec une largeur d'exactement 8 bits. D'autres représentations sont toujours prises en charge par la norme, mais pour d'autres types entiers.(int)value
comportement est défini par l'implémentation si le typeint
n'a que 16 bits. Je crains que vous n'ayez besoin de l'utiliser(long)value - 0x10000
, mais sur les architectures complémentaires non 2, la valeur0x8000 - 0x10000
ne peut pas être représentée en 16 bitsint
, donc le problème persiste.long
cela fonctionnerait tout aussi bien.Une autre méthode - en utilisant
union
:Au programme:
first_byte
etsecond_byte
peuvent être échangés selon le modèle petit ou grand endien. Cette méthode n'est pas meilleure mais est l'une des alternatives.la source
byte[2]
queint16_t
la taille est la même, il s'agit de l'un ou l'autre des deux ordres possibles, et non de certaines valeurs de position binaires arbitraires. Ainsi, vous pouvez au moins détecter au moment de la compilation la finalité de l'implémentation.Les opérateurs arithmétiques shift et expression au niveau du bit ou in
(uint16_t)data[0] | ((uint16_t)data[1] << 8)
ne fonctionnent pas sur les types plus petits queint
, de sorte que cesuint16_t
valeurs soient promues enint
(ouunsigned
sisizeof(uint16_t) == sizeof(int)
). Cependant, cela devrait donner la bonne réponse, car seuls les 2 octets inférieurs contiennent la valeur.Une autre version pédantiquement correcte pour la conversion big-endian en little-endian (en supposant que le processeur little-endian) est:
memcpy
est utilisé pour copier la représentation deint16_t
et c'est la manière conforme aux normes de le faire. Cette version se compile également en 1 instructionmovbe
, voir assemblage .la source
__builtin_bswap16
existe parce que l'échange d'octets dans ISO C ne peut pas être implémenté aussi efficacement.int16_t
enuint16_t
est bien définie: les valeurs négatives se convertissent en valeurs supérieures àINT_MAX
, mais la reconversion de ces valeurs enuint16_t
est un comportement défini par l'implémentation: 6.3.1.3 Entiers signés et non signés 1. Lorsqu'une valeur de type entier est convertie en un autre type entier autre que_Bool, si la valeur peut être représentée par le nouveau type, elle est inchangée. ... 3. Sinon, le nouveau type est signé et la valeur ne peut pas y être représentée; soit le résultat est défini par l'implémentation, soit un signal défini par l'implémentation est émis.ntohs
/__builtin_bswap
et le|
/<<
pattern: gcc.godbolt.org/z/rJ-j87Voici une autre version qui ne repose que sur des comportements portables et bien définis (l'en-tête
#include <endian.h>
n'est pas standard, le code l'est):La version little-endian se compile en
movbe
instruction unique avecclang
, lagcc
version est moins optimale, voir assemblage .la source
uint16_t
à laint16_t
conversion, cette version ne dispose pas de cette conversion, donc ici vous allez.Je tiens à remercier tous les contributeurs pour leurs réponses. Voici ce que le travail collectif se résume à:
uint8_t
,int16_t
etuint16_t
doivent utiliser la représentation du complément à deux sans aucun bit de remplissage, de sorte que les bits réels de la représentation sont sans ambiguïté ceux des 2 octets du tableau, dans l'ordre spécifié par les noms des fonctions.(unsigned)data[0] | ((unsigned)data[1] << 8)
(pour la petite version endienne) se compile en une seule instruction et produit une valeur 16 bits non signée.uint16_t
en type signéint16_t
a un comportement défini par l'implémentation si la valeur n'est pas dans la plage du type de destination. Aucune disposition particulière n'est prévue pour les types dont la représentation est définie avec précision.INT_MAX
et calculer la valeur signée correspondante en soustrayant0x10000
. Faire cela pour toutes les valeurs comme suggéré par zwol peut produire des valeurs en dehors de la plage deint16_t
avec le même comportement défini par l'implémentation.0x8000
bit entraîne explicitement les compilateurs à produire du code inefficace.memcpy
.En combinant les points 2 et 7, voici une solution portable et entièrement définie qui se compile efficacement en une seule instruction avec gcc et clang :
Assemblage 64 bits :
la source
char
types peuvent alias ou contenir la représentation d'objet de tout autre type.uint16_t
est pas l' un deschar
types, de sorte quememcpy
deuint16_t
àint16_t
est pas un comportement bien défini. La norme nécessite uniquement unechar[sizeof(T)] -> T > char[sizeof(T)]
conversion avecmemcpy
pour être bien définie.memcpy
ofuint16_t
toint16_t
est au mieux défini par l'implémentation, pas portable, pas bien défini, exactement comme l'affectation de l'un à l'autre, et vous ne pouvez pas par magie contourner cela avecmemcpy
. Peu importe siuint16_t
la représentation du complément à deux est utilisée ou non, ou si des bits de remplissage sont présents ou non - ce n'est pas un comportement défini ou requis par la norme C.r = u
parmemcpy(&r, &u, sizeof u)
mais ce dernier n'est pas meilleur que le premier, n'est-ce pas?