Qu'est-ce que la règle du strict aliasing et pourquoi votre code C++ peut soudainement se casser ?
\nAvez-vous déjà écrit un code C++ apparemment parfait, pour que le compilateur, à des niveaux d'optimisation élevés (-O2 ou -O3), le transforme en quelque chose de non fonctionnel ? Bienvenue dans le monde de l'aliasing strict (strict aliasing rule). C'est l'une des règles les plus sournoises du langage, capable de transformer le débogage en une véritable quête. Chez PythonLib, nous aimons expliquer les sujets complexes en termes simples, et l'article d'aujourd'hui ne fait pas exception.
Imaginons : vous avez deux pointeurs de types différents pointant vers la même zone mémoire. Le compilateur, suivant la règle du strict aliasing, a parfaitement le droit de décider qu'ils ne peuvent pas référencer le même objet. Et si vous enfreignez cette règle — attendez-vous à un comportement indéfini (UB). Le programme peut fonctionner, planter, ou afficher des résultats erronés. Cela semble effrayant ? Décortiquons cela.
\n\nL'essence de la règle : pourquoi le compilateur est-il si « strict » ?
\nLa règle du strict aliasing en C++ (et en C) stipule : accéder à un objet via un pointeur ou une référence d'un type incompatible est un UB. Il existe des exceptions uniquement pour char, unsigned char et std::byte. Pourquoi cela est-il nécessaire ? La réponse est simple : l'optimisation. Si le compilateur est certain que deux pointeurs de types différents ne peuvent pas pointer vers la même mémoire, il peut librement réorganiser les opérations, mettre en cache les valeurs dans les registres et optimiser agressivement le code. Sans cette règle, les performances de nombreux programmes chuteraient considérablement.
Mais il y a un revers : les développeurs qui tentent de faire du type punning (interpréter les mêmes octets comme des types différents) tombent souvent dans le piège. L'exemple classique est la tentative de vérifier rapidement si un nombre à virgule flottante est NaN, via un reinterpret_cast vers int. Le compilateur peut « casser » un tel code.
\n\nÀ quoi ressemble une violation du strict aliasing ? Exemples de code
\nConsidérons l'exemple le plus populaire, qui circule d'un Stack Overflow à l'autre :
\n#include <iostream>\n#include <cstdint>\n\nint main() {\n float f = 3.14f;\n // Violation du strict aliasing : lire un float comme un int\n int* pi = reinterpret_cast<int*>(&f);\n std::cout << *pi; // UB !\n return 0;\n}\n\nÀ première vue, c'est une astuce courante pour visualiser la représentation binaire d'un float. Mais la norme C++ dit : float et int sont des types incompatibles. Le compilateur peut optimiser ce code de sorte que vous obteniez des données incohérentes. Avec les optimisations activées, GCC, par exemple, peut simplement retourner une valeur prédéfinie ou même supprimer le code.
Un autre exemple — le travail avec des tableaux d'octets :
\nstruct Point { int x, y; };\nchar buffer[sizeof(Point)];\n// On remplit buffer...\nPoint* p = reinterpret_cast<Point*>(buffer);\np->x = 42; // UB, si buffer n'a pas été créé en tant que Point\n\nIci, la violation se produit car buffer est un tableau de char, et nous y accédons comme à un Point. Bien que char* puisse pointer vers n'importe quel objet, l'inverse n'est pas vrai : Point* ne peut pas pointer vers char[], à moins que la mémoire n'ait été allouée en tant que Point.
Comparaison des approches : comment contourner le strict aliasing en toute sécurité ?
\nIl existe plusieurs façons légales d'effectuer du type punning en C++. Nous comparerons trois approches principales : memcpy, union et std::bit_cast (C++20). Chacune a ses avantages et ses inconvénients.
1. memcpy — la bonne vieille méthode
\nLa méthode la plus sûre et portable consiste à copier les octets via memcpy. Les compilateurs optimisent très bien cet appel, le transformant souvent en un simple chargement de registre, donc il n'y a pas de surcharge :
#inc