63 votes

reinterpret_cast entre char * et std :: uint8_t * - safe?

Maintenant, nous sommes tous doivent parfois travailler avec des données binaires. En C++, nous travaillons avec des séquences d'octets, et depuis le début char a été la notre bloc de construction. Défini pour avoir sizeof de 1, c'est l'octet. Et toutes bibliothèque de fonctions d'e/S d'utilisation char par défaut. Tout est bon mais il y avait toujours un peu d'inquiétude, un peu de bizarrerie qui énerve certaines personnes - le nombre de bits dans un octet est mise en œuvre définies.

Donc en C99, il a été décidé d'introduire plusieurs typedefs les développeurs peuvent facilement s'exprimer, la largeur fixe les types d'entiers. En option, bien sûr, puisque nous ne voulons pas blesser la portabilité. Parmi eux, uint8_t, migré dans C++11 std::uint8_t, une largeur fixe de 8 bits type entier non signé, a été le choix parfait pour les gens qui voulaient vraiment travailler avec 8 bits octets.

Et donc, les développeurs ont adopté la nouvelle outils et a commencé la construction de bibliothèques que de manière expressive de l'état qu'ils acceptent de 8 bits de l'octet de séquences, comme std::uint8_t*, std::vector<std::uint8_t> ou autrement.

Mais, peut-être avec un très profond de la pensée, le comité de normalisation a décidé de ne pas exiger la mise en œuvre de l' std::char_traits<std::uint8_t> donc interdire aux développeurs de facilement et de façon portable de l'instanciation, disons, std::basic_fstream<std::uint8_t> et facilement la lecture de std::uint8_ts en tant que données binaires. Ou peut-être, certains d'entre nous ne se soucient pas le nombre de bits dans un octet, et sont heureux avec elle.

Mais malheureusement, deux mondes s'affrontent et parfois, vous avez à prendre des données en char* et passer à une bibliothèque qui attend std::uint8_t*. Mais attendez, vous dites, n'est-ce pas char variable bit et std::uint8_t est fixé à 8? Il va entraîner une perte de données?

Ainsi, il est intéressant Standardese sur cette. L' char défini pour contenir exactement un byte et octet est le plus bas adressable partie de la mémoire, donc il ne peut pas être un type avec peu de largeur moindre que celle de l' char. Ensuite, il est défini pour être en mesure de tenir UTF-8 unités de code. Cela nous donne le minimum de 8 bits. Alors maintenant, nous avons une définition de type qui est nécessaire pour être 8 bits de large et un type qui est au moins 8 bits de large. Mais y at-il des alternatives? Oui, unsigned char. Rappelez-vous que ce paramètre de char est mise en œuvre définies. Tout autre type? Heureusement, pas de. Tous les autres types intégraux ont besoin des plages qui sont à l'extérieur de 8 bits.

Enfin, std::uint8_t est facultative, ce qui signifie que la bibliothèque qui utilise ce type ne compilera pas si elle n'est pas définie. Mais si ça compile? Je peux dire avec un grand degré de confiance que cela signifie que nous sommes sur une plate-forme avec 8 bits, octets et CHAR_BIT == 8.

Une fois que nous avons cette connaissance, que nous avons octets de 8 bits, qui std::uint8_t est mis en application comme char ou unsigned char, peut-on supposer que nous pouvons faire, reinterpret_cast de char* de std::uint8_t* , et vice-versa? Est-il portable?

C'est là que mon Standardese les compétences en lecture à me manquer. J'ai lu en toute sécurité provenant des pointeurs ([basic.stc.dynamic.safety]) et, comme je le comprends, le suivant:

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

est sûr, si nous n'avons pas touch buffer2. Corrigez-moi si je me trompe.

Donc, étant donné les conditions suivantes:

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

Est-il portable et coffre-fort de jeter char* et std::uint8_t d'avant en arrière, en supposant que nous travaillons avec des données binaires et l'absence de signe d' char n'a pas d'importance?

Je vous serais reconnaissant de références à la Norme avec des explications.

EDIT: Merci, Jerry Cercueil. Je vais ajouter la citation de la Norme ([base.lval], §3.10/10):

Si un programme tente d'accéder à la valeur d'un objet par l'intermédiaire d'un glvalue d'autre que l'une des types suivants le comportement est indéfini:

...

- un char ou unsigned char type.

EDIT2: Ok, d'aller plus loin. std::uint8_t n'est pas garanti d'être un typedef d' unsigned char. Il peut être mis en œuvre prolongée de type entier non signé et étendu entier non signé types ne sont pas inclus dans le §3.10/10. Que faire maintenant?

EDIT3: Supprimé.

EDIT4: je crois que je vais garder cette question ouverte, parce que je ne suis pas sûr à 100% et il soulève un débat intéressant.

EDIT5: Ok, faire reinterpret_cast n'est pas dangereux en soi. C'est quand vous essayez de déférence pointeur résultant de la que les problèmes commencent à surgir. Donc nous avons besoin d'observer les cas possibles d'utilisation des données binaires en tant que char* et std::uint8_t*. Une erreur de vérification a été omis par souci de concision.

Cas 1:

#include <cassert>
#include <fstream>

int main()
{
    int value1 = 1234;

    {
        std::ofstream outputstream("test.bin", std::ios_base::binary);
        outputstream.write(reinterpret_cast<char*>(&value1), sizeof(value1));
    }

    int value2 = 1234;

    {
        std::ifstream inputstream("test.bin", std::ios_base::binary);
        inputstream.read(reinterpret_cast<char*>(&value2), sizeof(value2));
    }

    assert(value1 == value2);
}

Cas 2:

#include <cstdint>
#include <cassert>
#include <fstream>

class uint8_char_traits
{
    /* ... */
};

int main()
{
    int value1 = 1234;

    {
        std::basic_ofstream<std::uint8_t, uint8_char_traits> outputstream("test.bin", std::ios_base::binary);
        outputstream.write(reinterpret_cast<std::uint8_t*>(&value1), sizeof(value1));
    }

    int value2 = 1234;

    {
        std::basic_ifstream<std::uint8_t, uint8_char_traits> inputstream("test.bin", std::ios_base::binary);
        inputstream.read(reinterpret_cast<std::uint8_t*>(&value2), sizeof(value2));
    }

    assert(value1 == value2);
}

Cas 3:

#include <cstdint>
#include <cassert>
#include <fstream>
#include <memory>
#include <algorithm>

void library_function1(char* buffer, std::size_t size);
void library_function2(std::uint8_t* buffer, std::size_t size);

int main()
{
    const std::size_t size = sizeof(int);

    std::unique_ptr<char[]> data(new char[size]);

    {
        std::ifstream inputstream("test.bin", std::ios_base::binary);
        inputstream.read(data.get(), size);
    }

    library_function1(data.get(), size);
    library_function2(reinterpret_cast<std::uint8_t*>(data.get()), size);
}

void library_function1(char* buffer, std::size_t size)
{
    //Subcase 1:
    int value1 = *reinterpret_cast<int*>(buffer);

    //Subcase 2:
    int value2 = 0;
    std::memcpy(&value2, buffer, sizeof(int));

    //Subcase 3:
    int value3 = 0;
    char* ptr = reinterpret_cast<char*>(&value3);
    std::copy(buffer, buffer + sizeof(int), ptr);

    assert((value1 == value2) && (value2 == value3));
}

void library_function2(std::uint8_t* buffer, std::size_t size)
{
    //Subcase 4:
    int value1 = *reinterpret_cast<int*>(buffer);

    //Subcase 5:
    int value2 = 0;
    std::memcpy(&value2, buffer, sizeof(int));

    //Subcase 6:
    int value3 = 0;
    std::uint8_t* ptr = reinterpret_cast<std::uint8_t*>(&value3);
    std::copy(buffer, buffer + sizeof(int), ptr);

    assert((value1 == value2) && (value2 == value3));
}

Ces cas d'utilisation devrait permettre de donner une plus claire intention de ma question.

33voto

FaTony Points 594

Ok, passons vraiment pédant. Après la lecture de ceci, ceci et cela, je suis assez confiant que je comprends l'intention derrière les deux Normes.

Donc, en faisant reinterpret_cast de std::uint8_t* de char* puis déréférencement du pointeur résultant est sûr et portable et est explicitement autorisé par la [base.lval], §3.10/10.

Toutefois, reinterpret_cast de char* de std::uint8_t* puis déréférencement du pointeur résultant est une violation de la stricte aliasing règle et est un comportement indéfini si std::uint8_t est mis en œuvre comme prolongée de type entier non signé.

Cependant, il y a deux solutions possibles, tout d'abord:

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

Avec cette assertion en place, votre code ne compile pas sur les plates-formes sur lesquelles il serait d'entraîner un comportement non défini autrement.

Deuxième:

memcpy(uint8buffer, charbuffer, size);

Je ne trouve pas 100% libellé clair pourquoi est-ce sûr, mais jusqu'à présent, partout où je regardais, il est dit pour être sûr.

Pour rappel, afin d'être en mesure d' reinterpret_cast entre char* et std::uint8_t* et de travail résultant de pointeurs de façon portable et en toute sécurité à 100% standard conforme façon, les conditions suivantes doivent être remplies:

  • CHAR_BIT == 8.
  • std::uint8_t est défini.
  • std::uint8_t est mis en œuvre en tant que unsigned char.

Sur une note pratique, les conditions ci-dessus sont remplies sur 99% des plates-formes et il n'y a probablement pas de plate-forme sur laquelle les 2 premières conditions sont remplies, tandis que le 3e est faux.

EDIT: nous allons respecter notre cas d'utilisation.

Le cas 1 est en sécurité. Accéder au relevé des données est explicitement autorisé par la Norme.

Cas 2 peut être un comportement indéfini si std::uint8_t est mis en œuvre tel que prorogé type entier non signé.

Le cas 3 est la plus intéressante.

Subcases 1 et 4 sont toujours un comportement indéfini.

Litige subordonné 3 semble sûr de moi.

Litige subordonné 6 peut être un comportement indéfini.

Subcases 2 et 5 sont les plus susceptibles sûr, mais je ne peux pas trouver un libellé clair de preuves.

20voto

Jerry Coffin Points 237758

Si uint8_t existe, essentiellement le seul choix, c'est que c'est un typedef pour unsigned char (ou char si elle arrive à être non signé). Rien (mais un champ de bits) peut représenter moins d'espace de stockage qu'un char, et le seul autre type qui peut être aussi petit que 8 bits est un bool. La prochaine plus petite normale type integer est un short, qui doit être d'au moins 16 bits.

En tant que tel, si uint8_t existe à tous, vous avez vraiment seulement deux possibilités: soit vous êtes casting unsigned char de unsigned char, ou de coulée signed char de unsigned char.

Le premier est une identité de conversion, alors, évidemment, coffre-fort. Ce dernier relève de la "dérogation" donné pour accéder à n'importe quel autre type comme une séquence de char ou unsigned char dans le §3.10/10, donc il donne aussi un comportement défini.

Depuis que comprend à la fois char et unsigned char, d'un plâtre pour l'accès comme une séquence de char donne également le comportement défini.

Edit: autant Que Luc mention de l'étendue types integer va, je ne suis pas sûr comment vous pouvez gérer à appliquer pour obtenir une différence dans ce cas. C++ désigne le standard C99 pour les définitions de" uint8_t et un tel, de sorte que les citations dans le reste de ce venir à partir de C99.

§6.2.6.1/3 indique que unsigned char d'utiliser une pure représentation binaire, sans rembourrage bits. Rembourrage bits ne sont autorisés que dans 6.2.6.2/1, qui exclut expressément unsigned char. Cet article, cependant, décrit une pure représentation binaire en détail-littéralement à l'bits. Par conséquent, unsigned char et uint8_t (si elle existe) doit être représenté de manière identique au niveau du bit.

Pour voir une différence entre les deux, nous avons à affirmer que certains bits vu que l'on pourrait produire des résultats différents de ceux vus comme les autres, malgré le fait que les deux doivent être identiques sur les représentations au niveau du bit.

Pour le dire plus directement: une différence de résultat entre les deux exige qu'ils interprètent bits différemment -- malgré une direct exigence dont ils interprètent les bits de la même manière.

Même sur un niveau purement théorique, cela semble difficile à réaliser. Sur quelque chose d'approchant un niveau pratique, c'est évidemment ridicule.

0voto

Mehrdad Points 70493

J'ai posé une question il y a quelques semaines qui contient la réponse à la vôtre.

Regardez la réponse de R. qui explique que uint8_t n'a pas besoin d'avoir la même représentation que unsigned char .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X