Cette utilisation des syndicats est-elle ssortingctement conforme?

Compte tenu du code:

struct s1 {unsigned short x;}; struct s2 {unsigned short x;}; union s1s2 { struct s1 v1; struct s2 v2; }; static int read_s1x(struct s1 *p) { return p->x; } static void write_s2x(struct s2 *p, int v) { p->x=v;} int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3) { if (read_s1x(&p1->v1)) { unsigned short temp; temp = p3->v1.x; p3->v2.x = temp; write_s2x(&p2->v2,1234); temp = p3->v2.x; p3->v1.x = temp; } return read_s1x(&p1->v1); } int test2(int x) { union s1s2 q[2]; q->v1.x = 4321; return test(q,q+x,q+x); } #include  int main(void) { printf("%d\n",test2(0)); } 

Il existe un seul object union dans l’ensemble du programme– q . Son membre actif est défini sur v1 , puis sur v2 , puis à nouveau sur v1 . Le code utilise uniquement l’opérateur adresse-de sur q.v1 , ou le pointeur résultant, lorsque ce membre est actif, et de même q.v2 . Puisque p1 , p2 et p3 sont tous du même type, il devrait être parfaitement légal d’utiliser p3->v1 pour accéder à p1->v1 et p3->v2 pour accéder à p2->v2 .

Je ne vois rien qui justifierait qu’un compilateur ne produise pas la sortie 1234, mais de nombreux compilateurs, y compris clang et gcc, génèrent du code qui produira 4321. Je pense que ce qui se passe, c’est qu’ils décident que les opérations sur p3 ne changeront pas le contenu. de n’importe quel bit en mémoire, ils peuvent simplement être ignorés, mais je ne vois rien dans la norme qui justifierait d’ignorer le fait que p3 est utilisé pour copier des données de p1->v1 vers p2->v2 et inversement.

Existe-t-il quelque chose dans la norme qui justifierait un tel comportement, ou les compilateurs ne le suivent-ils tout simplement pas?

Je crois que votre code est conforme et qu’il y a une faille dans le mode -fssortingct-aliasing de GCC et Clang.

Je ne trouve pas la bonne partie de la norme C, mais le même problème se produit lors de la compilation de votre code en mode C ++ pour moi et j’ai trouvé les passages pertinents de la norme C ++.

Dans la norme C ++, [class.union] / 5 définit ce qui se produit lorsque l’opérateur = est utilisé dans une expression d’access union. La norme C ++ stipule que, lorsqu’une union est impliquée dans l’expression d’access de membre de l’opérateur intégré = , le membre actif de l’union est remplacé par le membre impliqué dans l’expression (si le type a un constructeur sortingvial, mais parce que C est du code C, il a un constructeur sortingvial).

Notez que write_s2x ne peut pas changer le membre actif de l’union, car une union n’est pas impliquée dans l’expression d’affectation. Votre code ne suppose pas que cela se produit, alors c’est OK.

Même si j’utilise placement new pour changer explicitement quel membre de l’union est actif, ce qui devrait indiquer au compilateur que le membre actif a changé, GCC continue de générer du code qui génère 4321 .

Cela ressemble à un bogue avec GCC et Clang, en supposant que le changement de membre actif de l’union ne peut pas se produire ici, car ils ne reconnaissent pas la possibilité que p1 , p2 et p3 pointent tous sur le même object.

GCC et Clang (et à peu près tous les autres compilateurs) prennent en charge une extension en C / C ++ dans laquelle vous pouvez lire un membre inactif d’une union (obtenir toute valeur potentiellement erronée en conséquence), mais uniquement si vous effectuez cet access dans un access membre. expression impliquant le syndicat. Si v1 n’était pas le membre actif, le read_s1x ne serait pas défini par cette règle spécifique à l’implémentation, car l’union ne se read_s1x pas dans l’expression d’access de membre. Mais comme v1 est le membre actif, cela ne devrait pas avoir d’importance.

C’est un cas compliqué, et j’espère que mon parsing est correcte, en tant que personne qui n’est pas responsable de la maintenance du compilateur ou membre d’un des comités.

Avec une interprétation ssortingcte de la norme, ce code pourrait ne pas être conforme . Concentrons nous sur le texte du §6.5p7 bien connu:

Un object doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui possède l’un des types suivants:
– un type compatible avec le type effectif de l’object,
– une version qualifiée d’un type compatible avec le type effectif de l’object,
– un type qui est le type signé ou non signé correspondant au type effectif de l’object,
– un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l’object,
un type d’agrégat ou d’union qui inclut l’un des types mentionnés ci-dessus parmi ses membres (y compris, de manière récursive, un membre d’une union sous-agrégée ou confinée), ou
– un type de caractère.

(c’est moi qui souligne)

Vos fonctions read_s1x() et write_s2x() font l’ inverse de ce que j’ai marqué en gras ci-dessus dans le contexte de votre code entier . Avec juste ce paragraphe, vous pourriez conclure que ce n’est pas autorisé: Un pointeur sur l’ union s1s2 serait autorisé à union s1s2 un pointeur sur la struct s1 , mais pas l’inverse.

Cette interprétation signifierait bien sûr que le code doit fonctionner comme prévu si vous “insérez” ces fonctions manuellement dans votre test() . C’est en effet le cas ici avec gcc 6.2 pour i686-w64-mingw32 .


Ajout de deux arguments en faveur de l’interprétation ssortingcte présentée ci-dessus:

  • Bien qu’il soit toujours permis d’aliaser n’importe quel pointeur avec char * , un tableau de caractères ne peut être aliasé par aucun autre type.

  • Considérant le §6.5.2.3p6 (non lié ici):

    Une garantie spéciale est donnée afin de simplifier l’utilisation des unions: si une union contient plusieurs structures partageant une séquence initiale commune (voir ci-dessous), et si l’object union contient actuellement l’une de ces structures, il est autorisé à inspecter la structure commune. partie initiale de l’un d’entre eux où qu’une déclaration du type complet de l’union soit visible.

    (Là encore, c’est moi qui souligne) – l’interprétation typique est qu’être visible signifie directement dans le cadre de la fonction en question, et non “quelque part dans l’unité de traduction” … donc cette garantie n’inclut pas une fonction qui prend un pointeur vers une des struct qui est un membre du union .

Je n’ai pas lu la norme, mais il est dangereux de jouer avec des pointeurs en mode alias ssortingct (c’est-à -fssortingct-alising dire, en utilisant -fssortingct-alising ). Voir le doc en ligne de gcc :

Portez une attention particulière à un code comme celui-ci:

 union a_union { int i; double d; }; int f() { union a_union t; td = 3.0; return ti; } 

La pratique consistant à lire à partir d’un autre membre du syndicat que celui type-punning le plus récemment (appelé « type-punning ) est courante. Même avec -fssortingct-aliasing , le -fssortingct-aliasing est autorisé, à condition que la mémoire soit accessible via le type d’union. Ainsi, le code ci-dessus fonctionne comme prévu. Reportez-vous à la section Implémentation des énumérations des unions et des champs de bits Cependant, ce code pourrait ne pas:

 int f() { union a_union t; int* ip; td = 3.0; ip = &t.i; return *ip; } 

De même, l’access en prenant l’adresse, en convertissant le pointeur résultant et en déréférencant le résultat a un comportement indéfini, même si la conversion utilise un type d’union, par exemple:

 int f() { double d = 3.0; return ((union a_union *) &d)->i; } 

L’option -fssortingct-aliasing est activée aux niveaux -O2, -O3, -Os.

Trouvé quelque chose de similaire dans le deuxième exemple hein?

Il ne s’agit pas de se conformer ou de ne pas se conformer – c’est l’un des “pièges” d’optimisation. Toutes vos structures de données ont été optimisées et vous passez le même pointeur à l’optimisation des données afin que l’arborescence d’exécution soit réduite à une simple impression de la valeur.

  sub rsp, 8 mov esi, 4321 mov edi, OFFSET FLAT:.LC0 xor eax, eax call printf xor eax, eax add rsp, 8 ret 

pour le changer, il faut que cette fonction de “transfert” soit sujette aux effets secondaires et force les assignations réelles. Cela forcera l’optimiseur à ne pas réduire les nœuds de l’arborescence d’exécution:

 int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3) /* ....*/ main: sub rsp, 8 mov esi, 1234 mov edi, OFFSET FLAT:.LC0 xor eax, eax call printf xor eax, eax add rsp, 8 ret 

C’est un test assez sortingvial, juste artificiellement un peu plus compliqué.