web-dev-qa-db-fra.com

Est-ce que std :: memcpy entre différents types de comportement trivialement copiable est indéfini?

J'utilise std::memcpy Pour contourner l'aliasing strict depuis longtemps.

Par exemple, inspecter un float, comme this :

float f = ...;
uint32_t i;
static_assert(sizeof(f)==sizeof(i));
std::memcpy(&i, &f, sizeof(i));
// use i to extract f's sign, exponent & significand

Cependant, cette fois-ci, j'ai vérifié le standard, je n'ai rien trouvé qui puisse le valider. Tout ce que j'ai trouvé c'est this :

Pour tout objet (autre qu'un sous-objet potentiellement chevauchant) de type trivialement copiable T, que l'objet contienne ou non une valeur valide de type T, les octets sous-jacents ([intro.memory]) constituant l'objet peuvent être copiés dans un fichier. tableau de char, char non signé ou std :: byte ([cstddef.syn]).40 Si le contenu de ce tableau est recopié dans l'objet, celui-ci conservera ensuite sa valeur d'origine. [ Exemple:

#define N sizeof(T)
char buf[N];
T obj;                          // obj initialized to its original value
std::memcpy(buf, &obj, N);      // between these two calls to std​::​memcpy, obj might be modified
std::memcpy(&obj, buf, N);      // at this point, each subobject of obj of scalar type holds its original value

- fin exemple]

et this :

Pour tout type trivialement copiable T, si deux pointeurs vers T pointent vers des objets T distincts obj1 et obj2, où ni obj1 ni obj2 ne sont des sous-objets susceptibles de se chevaucher, si les octets sous-jacents ([intro.memory]) constituant obj1 sont copiés dans obj2,41 obj2 doit par la suite avoir la même valeur que obj1. [ Exemple:

T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p

- fin exemple]

Ainsi, std::memcpy Une float to/from char[] Est autorisée, et std::memcpy Entre les mêmes types triviaux est également autorisée.

Mon premier exemple (et la réponse associée) sont-ils bien définis? Ou bien, la méthode correcte pour inspecter un float consiste à std::memcpy Dans un tampon unsigned char[], Et en utilisant shifts et ors pour créer un uint32_t À partir de cela?


Remarque: consulter les garanties de std::memcpy Peut ne pas répondre à cette question. Autant que je sache, je pourrais remplacer std::memcpy Par une simple boucle de copie d'octets et la question sera la même.

51
geza

La norme peut échouer à dire correctement que cela est autorisé, mais c'est probablement supposé l'être, et à ma connaissance, toutes les implémentations traiteront cela comme un comportement défini.

Afin de faciliter la copie dans un fichier char[N] objet, les octets constituant l’objet f sont accessibles comme s’il s’agissait d’un objet char[N]. Je pense que cette partie n’est pas contestée.

Octets d'un char[N] qui représente un uint32_t La valeur peut être copiée dans un uint32_t objet. Je pense que cette partie n’est pas non plus contestée.

Je crois également que le fait, par exemple, de fwrite peut avoir écrit les octets dans une exécution du programme et fread peut les avoir lus lors d'une autre exécution, voire même d'un autre programme.

En raison de cette dernière partie, je pense que la provenance des octets importe peu, tant qu’ils constituent une représentation valide de certains uint32_t objet. Vous auriez p avoir parcouru toutes les valeurs float, en utilisant memcmp sur chacune jusqu'à ce que vous obteniez la représentation souhaitée, que vous saviez identique à celle du paramètre uint32_t valeur que vous interprétez comme. Vous pourriez même l'avoir fait dans un autre programme, un programme que le compilateur n'a jamais vu. Cela aurait été valide.

Si, du point de vue de l'implémentation, votre code est impossible à distinguer du code valide sans ambiguïté, votre code doit être considéré comme valide.

21
user743382

Mon premier exemple (et la réponse associée) sont-ils bien définis?

Le comportement n'est pas indéfini (à moins que le type de cible ait des représentations d'interruption qui ne sont pas partagés par le type de source), mais la valeur résultante de l’entier est définie par l’implémentation. Standard ne donne aucune garantie quant à la représentation des nombres en virgule flottante. Il est donc impossible d'extraire de manière portable la mantisse, etc. de l'entier. Cela dit, vous limiter à IEEE 754 à l'aide de systèmes ne vous limite pas beaucoup ces jours-ci.

Problèmes de portabilité:

  • IEEE 754 n'est pas garanti par C++
  • L'endianité d'octet de float n'est pas garantie pour correspondre à l'endianité entière.
  • (Systèmes avec représentations de piège).

Vous pouvez utiliser std::numeric_limits::is_iec559 pour vérifier si votre hypothèse concernant la représentation est correcte.

 Bien que, il semble que uint32_t ne peut pas avoir de pièges (voir commentaires), vous n'avez donc pas à vous en soucier. En utilisant uint32_t, vous avez déjà exclu la portabilité vers des systèmes ésotériques - les systèmes conformes à la norme ne sont pas tenus de définir cet alias.

18
eerorika

Votre exemple est bien défini et ne supprime pas le crénelage strict. std::memcpy indique clairement:

Copie count octets de l'objet pointé par src à l'objet pointé par dest. Les deux objets sont réinterprétés en tant que tableaux de unsigned char.

La norme permet l’aliasing de tout type via un (signed/unsigned) char* ou std::byte et votre exemple ne montre donc pas UB. Si le nombre résultant est de n'importe quelle valeur, c'est une autre question.


use i to extract f's sign, exponent & significand

Cela n’est cependant pas garanti par la norme car la valeur de float est définie par l’implémentation (dans le cas de IEEE 754, cela fonctionnera).

14
Sombrero Chicken