web-dev-qa-db-fra.com

reinterpret_cast entre char * et std :: uint8_t * - sûr?

Maintenant, nous devons tous parfois travailler avec des données binaires. En C++, nous travaillons avec des séquences d'octets, et depuis le début char était notre bloc de construction. Défini pour avoir sizeof de 1, c'est l'octet. Et toutes les fonctions d'E/S de la bibliothèque utilisent char par défaut. Tout va bien, mais il y avait toujours une petite inquiétude, une petite bizarrerie qui dérangeait certaines personnes - le nombre de bits dans un octet est défini par l'implémentation.

Ainsi, en C99, il a été décidé d'introduire plusieurs typedefs pour permettre aux développeurs de s'exprimer facilement, les types entiers à largeur fixe. Facultatif, bien sûr, car nous ne voulons jamais nuire à la portabilité. Parmi eux, uint8_t, migré en C++ 11 en tant que std::uint8_t, un type entier non signé 8 bits de largeur fixe, était le choix parfait pour les personnes qui voulaient vraiment travailler avec des octets 8 bits.

Ainsi, les développeurs ont adopté les nouveaux outils et ont commencé à créer des bibliothèques qui déclarent expressément qu'elles acceptent les séquences d'octets 8 bits, comme std::uint8_t*, std::vector<std::uint8_t> ou autrement.

Mais, peut-être avec une réflexion très approfondie, le comité de normalisation a décidé de ne pas exiger la mise en œuvre de std::char_traits<std::uint8_t> interdisant donc aux développeurs d'instancier facilement et de manière portative, disons std::basic_fstream<std::uint8_t> et lire facilement std::uint8_ts en tant que données binaires. Ou peut-être que certains d'entre nous ne se soucient pas du nombre de bits dans un octet et en sont satisfaits.

Mais malheureusement, deux mondes entrent en collision et parfois vous devez prendre des données comme char* et le transmettre à une bibliothèque qui attend std::uint8_t*. Mais attendez, vous dites, n'est pas char bit variable et std::uint8_t est fixé à 8? Cela entraînera-t-il une perte de données?

Eh bien, il y a un Standardese intéressant à ce sujet. Le char défini pour contenir exactement un octet et l'octet est le bloc de mémoire adressable le plus bas, donc il ne peut pas y avoir de type avec une largeur de bit inférieure à celle de char. Ensuite, il est défini pour pouvoir contenir des unités de code UTF-8. Cela nous donne le minimum - 8 bits. Nous avons donc maintenant un typedef qui doit avoir une largeur de 8 bits et un type d'au moins 8 bits. Mais existe-t-il des alternatives? Oui, unsigned char. N'oubliez pas que la signature de char est définie par l'implémentation. Tout autre type? Heureusement, non. Tous les autres types intégraux ont des plages requises qui se situent en dehors de 8 bits.

Finalement, std::uint8_t est facultatif, cela signifie que la bibliothèque qui utilise ce type ne se compilera pas si elle n'est pas définie. Mais que se passe-t-il s'il compile? Je peux dire avec une grande confiance que cela signifie que nous sommes sur une plate-forme avec 8 octets et CHAR_BIT == 8.

Une fois que nous avons cette connaissance, que nous avons des octets de 8 bits, que std::uint8_t est implémenté en tant que char ou unsigned char, pouvons-nous supposer que nous pouvons faire reinterpret_cast de char* à std::uint8_t* et vice versa? Est-ce portable?

C'est là que mes compétences en lecture standardese me manquent. J'ai lu des pointeurs dérivés en toute sécurité ([basic.stc.dynamic.safety]) et, si je comprends bien, ce qui suit:

std::uint8_t* buffer = /* ... */ ;
char* buffer2 = reinterpret_cast<char*>(buffer);
std::uint8_t buffer3 = reinterpret_cast<std::uint8_t*>(buffer2);

est sûr si on ne touche pas buffer2. Corrige moi si je me trompe.

Donc, étant donné les conditions préalables suivantes:

  • CHAR_BIT == 8
  • std::uint8_t est défini.

Est-il portable et sûr de lancer char* et std::uint8_t* d'avant en arrière, en supposant que nous travaillons avec des données binaires et que le manque potentiel de signe de char n'a pas d'importance?

J'apprécierais les références à la norme avec des explications.

EDIT: Merci, Jerry Coffin. Je vais ajouter la citation du Standard ([basic.lval], §3.10/10):

Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas défini:

...

- un type de caractère char ou unsigned char.

EDIT2: Ok, aller plus loin. std::uint8_t n'est pas garanti comme étant un typedef de unsigned char. Il peut être implémenté comme type entier non signé étend et les types entiers non signés étendus ne sont pas inclus dans le §3.10/10. Et maintenant?

59
Lyberta

Ok, soyons vraiment pédants. Après avoir lu this , this and this , je suis assez confiant que je comprends l'intention derrière les deux normes.

Donc, faire reinterpret_cast de std::uint8_t* à char*, puis déréférencer le pointeur résultant est sûr et portable et est explicitement autorisé par [basic.lval] .

Cependant, faire reinterpret_cast de char* à std::uint8_t*, puis déréférencer le pointeur résultant est une violation de la règle d'aliasing stricte et est un comportement non défini si std::uint8_t est implémenté comme type entier non signé étendu.

Cependant, il existe deux solutions possibles, premièrement:

static_assert(std::is_same_v<std::uint8_t, char> ||
    std::is_same_v<std::uint8_t, unsigned char>,
    "This library requires std::uint8_t to be implemented as char or unsigned char.");

Avec cette affirmation en place, votre code ne sera pas compilé sur les plates-formes sur lesquelles il en résulterait sinon un comportement indéfini.

Seconde:

std::memcpy(uint8buffer, charbuffer, size);

Cppreference dit que std::memcpy accède aux objets sous forme de tableaux de unsigned char il est donc sûr et portable .

Pour réitérer, afin de pouvoir reinterpret_cast entre char* et std::uint8_t* et travailler avec les pointeurs résultants de manière portable et en toute sécurité dans un 100% manière conforme aux normes, les conditions suivantes doivent être remplies:

  • CHAR_BIT == 8.
  • std::uint8_t est défini.
  • std::uint8_t est implémenté comme char ou unsigned char.

Sur le plan pratique, les conditions ci-dessus sont vraies sur 99% des plates-formes et il n'y a probablement aucune plate-forme sur laquelle les 2 premières conditions sont vraies tandis que la 3ème est fausse.

24
Lyberta

Si uint8_t existe du tout, essentiellement le seul choix est que c'est un typedef pour unsigned char (ou char s'il s'avère non signé). Rien (sauf un champ de bits) ne peut représenter moins de stockage qu'un char, et le seul autre type pouvant être aussi petit que 8 bits est un bool. Le type d'entier normal suivant le plus petit est un short, qui doit être d'au moins 16 bits.

En tant que tel, si uint8_t existe, vous n'avez vraiment que deux possibilités: vous lancez unsigned char à unsigned char ou casting signed char à unsigned char.

Le premier est une conversion d'identité, donc évidemment sûr. Ce dernier fait partie de la "dérogation spéciale" accordée pour accéder à tout autre type en tant que séquence de caractères ou de caractères non signés au §3.10/10, de sorte qu'il donne également un comportement défini.

Étant donné que cela inclut à la fois char et unsigned char, une distribution pour y accéder en tant que séquence de caractères donne également un comportement défini.

Edit: En ce qui concerne la mention de Luc des types entiers étendus, je ne sais pas comment vous pourriez l'appliquer pour obtenir une différence dans ce cas. C++ fait référence à la norme C99 pour les définitions de uint8_t et ainsi de suite, donc les citations dans le reste viennent de C99.

Le §6.2.6.1/3 précise que unsigned char doit utiliser une représentation binaire pure, sans bits de remplissage. Les bits de remplissage ne sont autorisés que dans 6.2.6.2/1, ce qui exclut spécifiquement unsigned char. Cette section, cependant, décrit une représentation binaire pure en détail - littéralement au bit. Donc, unsigned char et uint8_t (s'il existe) doit être représenté de façon identique au niveau du bit.

Pour voir une différence entre les deux, nous devons affirmer que certains bits particuliers lorsqu'ils sont vus comme l'un produiraient des résultats différents de ceux vus comme l'autre - malgré le fait que les deux doivent avoir des représentations identiques au niveau du bit.

Pour le dire plus directement: une différence de résultat entre les deux exige qu'ils interprètent les bits différemment - malgré l'exigence directe qu'ils interprètent les bits de manière identique.

Même à un niveau purement théorique, cela semble difficile à réaliser. Sur tout ce qui approche d'un niveau pratique, c'est évidemment ridicule.

19
Jerry Coffin