Contexte et définition du type punning

Le type punning consiste à lire une zone mémoire avec un type différent de celui utilisé lors de l’écriture. Cette technique apparaît dans la sérialisation, les protocoles réseau ou l’accès matériel. En pratique, le code fonctionne souvent, mais le standard C et C++ distingue clairement comportement défini et comportement indéfini. Le blog de Steve Schnepp montre qu’un même fragment de code peut produire des résultats différents selon le niveau d’optimisation du compilateur.

Méthodes sûres en C

Le standard C autorise deux constructions qui garantissent un comportement défini. La première est l’union : écrire dans un membre et lire dans un autre est explicitement permis. Exemple tiré de l’article :

union { float f; uint32_t bits; } pun;
pun.f = 3.14f;
uint32_t exp = (pun.bits >> 23) & 0xff; // extrait l’exposant IEEE‑754

Cette approche fonctionne également pour transformer une structure en tableau de flottants grâce à un second membre union. La seconde méthode est memcpy, qui copie les octets sans violer les règles d’aliasing. Le compilateur optimise souvent cet appel en un simple déplacement de registre :

float f = 3.14f;
int i;
memcpy(&i, &f, sizeof(f)); // comportement défini, compile en une instruction

Pointer casts et comportement indéfini

Le cast de pointeur vers un type non apparenté constitue un undefined behavior selon la règle du strict aliasing : un objet ne doit être accédé que via un lvalue de son type effectif, une version qualifiée ou un type caractère. Le code suivant illustre le problème :

float f = 3.14f;
int *p = (int *)&f;
int i = *p; // UB, même si le résultat semble correct sur la plupart des plateformes

À l’optimisation -O2, le compilateur peut supposer que l’accès via int* ne modifie pas le float original, ce qui conduit à la suppression ou à la réorganisation du code. Le bug décrit par l’auteur provient exactement de ce mécanisme : dans une boucle chaude, le cast float*uint32_t* a été éliminé, les écritures n’ont jamais atteint la mémoire attendue.

Différences C / C++ et impact de l’optimisation

En C, le type sert surtout à interpréter la mémoire ; le standard tolère l’usage d’union ou de memcpy pour le punning. En C++, les types sont « first‑class » : le compilateur peut supposer que deux types différents n’aliasent jamais. L’exemple du blog montre un fonction bar où un uint64_t* et un struct c* pointent sur la même zone. Sous GCC/Clang -O2, le test c->a == 2 après l’écriture *u64 = 4 est optimisé hors du flux, car le compilateur estime que l’écriture ne peut pas affecter c->a. Le même code compile à -O1 et renvoie 0, révélant la dépendance à la règle d’aliasing.

La conclusion pratique est claire : pour le punning, privilégier les unions ou memcpy, même en C++. Les casts de pointeur restent une source d’instabilité, surtout avec les optimisations agressives du compilateur. Remplacer le cast problématique par une union a résolu le bug en quelques minutes, comme le confirme l’auteur.