6
votes

La réinterprétation de la valeur castée varie selon le compilateur

Pour le même programme:

const char* s = "abcd";
auto x1 = reinterpret_cast<const int64_t*>(s);
auto x2 = reinterpret_cast<const char*>(x1);
std::cout << *x1 << std::endl;
std::cout << x2 << std::endl; // Always "abcd"

Dans gcc5 ( lien ): 139639660962401
Dans gcc8 ( lien ): 1684234849

  1. Pourquoi la valeur varie-t-elle selon les différentes versions du compilateur?
  2. Quel est alors un moyen sûr pour le compilateur de passer de const char * à int64_t et en arrière (comme dans ce problème - pas pour les chaînes entières réelles mais une avec d'autres caractères également)?


15 commentaires

Accéder à * x1 est un comportement indéfini . s pointe vers la mémoire qui ne dispose que de 5 octets alloués pour les données de chaîne. La lecture à partir de * x1 essaie d'accéder à la mémoire sur 8 octets à la place. Les 3 derniers octets ne sont pas définis et peuvent même ne pas être alloués, selon la façon dont les données de chaîne sont gérées.


Vous ne respectez pas l'aliasing strict (UB). Vous n'êtes pas autorisé à faire * x1 car vous ne pointez pas réellement vers un int64_t . Qu'essayez-vous réellement d'accomplir en faisant cela?


Vous pouvez obtenir plus d'informations en imprimant * x1 en hexadécimal.


Soyez prudent avec reinterpret_cast . Le compilateur vous permettra de réinterpréter beaucoup de choses en beaucoup de choses que vous ne devriez pas déréférencer. Il existe beaucoup de règles pour utiliser reinterpret_cast et la liste des choses que vous pouvez faire en toute sécurité dans la pratique est plutôt courte.


y a-t-il alors une manière correcte de faire (2)? Parce que jusqu'à 8 octets, une séquence char devrait être convertible en int64_t, n'est-ce pas?


Pour info, "abcd" ne fait que 5 octets, 0x61 0x62 0x63 0x64 0x00 . 139639660962401 est hexadécimal 0x00007F0064636261 tandis que 1684234849 est hexadécimal 0x0000000064636261 Vous pouvez voir que les deux valeurs numériques incluent les mêmes 5 octets de la chaîne "abcd" que prévu , mais le plus grand numérique inclut également une valeur d'octet aléatoire 0x7F après le terminateur nul, car le pointeur int64_t * accède à une mémoire non définie, alors que le plus petit numérique n'a pas cet aléatoire valeur d'octet dedans.


y a-t-il alors une manière correcte de faire (2)? Parce que jusqu'à 8 octets, une séquence de caractères doit être convertible en int64_t, n'est-ce pas? Non. À moins que vous ne commenciez par un int64_t , vous n'en avez pas et la lecture de la mémoire est UB. Quel problème essayez-vous de résoudre?


La manière politiquement correcte d'accéder à 8 octets conséquents en un seul int64_t consiste à utiliser memcpy () au lieu d'un type-cast


Je suggérerais une fonction simple comme uint64_t chars_to_int (const std :: string & string) {uint64_t return_value = 0; for (const auto & a: string) {return_value + = a; return_value << = (sizeof (char) * 8); } return return_value; }


@tangy dans le lien cppreference , (2) ne lance pas un pointeur vers un pointeur uint64_t * , comme vous essayez de le faire. Il lance à la place un pointeur vers un uintptr_t , qui n'est PAS en soi un pointeur du tout, mais qui est juste un entier dont la taille en octets est suffisamment grande pour contenir des valeurs de pointeur. C'est une très grosse différence. Ne laissez pas le ptr dans le nom du type vous tromper. uintptr_t est juste un alias pour uint32_t sur les plates-formes 32 bits et uint64_t sur les plates-formes 64 bits (ou équivalent).


@tangy auto x1 = reinterpret_cast (s); std :: cout << x1 << std :: endl; auto x2 = reinterpret_cast (x1); est parfaitement sûr.


Merci RemyLebeau Unterfliege pour les suggestions de solutions et autres pour la discussion. Vous pourriez l'ajouter comme réponse afin que cela puisse aider les autres?


@NathanOliver Je dois stocker cela dans un ensemble de données n'attendant que des types intégraux et il est garanti que le const char * sera toujours <8.


@RemyLebeau Vous devriez soumettre votre premier commentaire comme réponse, car c'est le plus significatif et le plus utile.


@tangy pour stocker le contenu de la chaîne dans un entier de 64 bits, vous devez vous assurer que les données de la chaîne sont d'au moins 8 octets lors de l'utilisation d'un cast de type pointeur, sinon vous devez memcpy () les données de chaîne dans une variable (u) int64_t séparée (ce qui est la meilleure façon de procéder).


3 Réponses :


6
votes
  1. Pourquoi la valeur varie-t-elle selon les différentes versions du compilateur?

Le comportement n'est pas défini.

  1. Quel est alors un moyen sûr pour le compilateur de passer de const char * à int64_t et en arrière

Ce que vous entendez par "déplacer de const char * vers int64_t" n'est pas clair. Sur la base de l'exemple, je suppose que vous voulez créer un mappage à partir d'une séquence de caractères (d'une longueur non supérieure à celle des ajustements) en un entier de 64 bits d'une manière qui peut être reconvertie à l'aide d'un autre processus - éventuellement compilée par une autre (version de) compilateur.

Commencez par créer un objet int64_t , initialisez à zéro:

auto back = reinterpret_cast<char*>(&i);

Obtenir la longueur de la chaîne p >

memcpy(&i, s, len);

Vérifiez qu'il convient

assert(len < sizeof i);

Copiez les octets de la séquence de caractères sur l'entier

auto len = strlen(s);

7 commentaires

Notez également que vous ne rencontrerez jamais une machine à complément non-deux ni une machine où un octet n'est pas 8 bits.


@okovko: Ceux-ci seront capturés au moment de la compilation: int64_t i = 0; ne compilera pas à moins que la machine ne soit un complément à deux, et 64 bits est un nombre entier d'octets.


@okovko sauf lorsque vous le faites .


@BenVoigt C'est bon à savoir, mais je pense qu'eerorika parlait de passer une valeur entre les machines d'un réseau, ce qui empêche la compilation. Cela dit, il n'y a aucune raison de s'inquiéter du complément ni de la taille des octets, car ce ne sont tout simplement pas des variables en 2019.


@eerorika me fait savoir le jour où vous transmettez une valeur à un DSP Texas Instruments sur un réseau.


Le réseau @okovko n'est qu'un exemple de transfert de données. Il existe sûrement des interfaces pour connecter un PC à un DSP? Je conviens que les rencontres que vous évoquez sont rares, mais pas impossibles.


Bien sûr, mais cela sort du cadre de la discussion sur la portabilité.



2
votes

Lorsque vous déréférencer le pointeur int64_t , il lit au-delà de la fin de la mémoire allouée pour la chaîne à partir de laquelle vous avez transtypé. Si vous modifiez la longueur de la chaîne à au moins 8 octets, la valeur entière deviendrait stable.

const char* s = "abcd";
auto x1 = reinterpret_cast<intptr_t>(s);
auto x2 = reinterpret_cast<const char*>(x1);
std::cout << x1 << std::endl;
std::cout << x2 << std::endl; // Always "abcd"

Si vous souhaitez stocker le pointeur dans un entier, vous devez utiliser intptr_t et omettez le * comme:

const char* s = "abcdefg"; // plus null terminator
auto x1 = reinterpret_cast<const int64_t*>(s);
auto x2 = reinterpret_cast<const char*>(x1);
std::cout << *x1 << std::endl;
std::cout << x2 << std::endl; // Always "abcd"


7 commentaires

Le comportement n'est toujours pas défini. int64_t n'est pas autorisé à utiliser un char (array). En outre, il n'est pas garanti que le tableau réponde aux exigences d'alignement de int64_t .


@eerorika Je comprends la restriction d'alignement, mais pas celle d'aliasing.


@eerorika L'alignement est bien défini si le tableau char est déclaré en haut de la portée englobante. Si vous aviez quelque chose comme char a; const char * s = "abcd"; alors vous obtiendrez une exception CPU sur diverses architectures ARM.


@okovko C ++ ne garantit pas si la trame d'appel est alignée sur une limite, ni ne garantit l'ordre des variables locales en mémoire. Votre ABI peut vous donner des garanties, que vous pourriez tenir pour acquises si cela ne vous dérange pas de restreindre la portabilité. Quoi qu'il en soit, il est assez simple de spécifier l'alignement si vous en avez besoin.


@eerorika Il existe un concept sous-estimé des comportements stables et normalisés des machines et des compilateurs sur lesquels les programmeurs peuvent s'appuyer, même s'ils ne sont pas spécifiés. C'est le vrai "standard" parce qu'il est réellement implémenté.


@okovko bonne chance lorsque vous faites du bughunting lorsque votre compilateur mis à jour décide à l'avenir de faire une optimisation plus agressive et qu'un comportement non défini suit son chemin ;-P eerorika a parfaitement raison


@ phön D'accord, chasseur de fantômes.



0
votes

Sur la base de ce que RemyLebeau a souligné dans les commentaires de votre message,

unsigned 5_byte_mask = 0xFFFFFFFFFF; std :: cout << * x1 & 5_byte_mask << std :: endl;

Cela devrait être un moyen raisonnable d'obtenir la même valeur sur une petite machine endian avec n'importe quel compilateur. Cela peut être UB selon une spécification ou une autre, mais du point de vue d'un compilateur, vous déréférencer huit octets à une adresse valide dont vous avez initialisé cinq octets et masquer les octets restants qui sont des données non initialisées / indésirables.


0 commentaires