web-dev-qa-db-fra.com

Pourquoi est-il invalide qu'un type d'union déclaré dans une fonction soit utilisé dans une autre fonction?

Quand j'ai lu ISO/IEC 9899: 1999 (voir: 6.5.2.3), j'ai vu un exemple comme celui-ci (c'est moi qui souligne):

Ce qui suit est pas un fragment valide (car le type d'union est non visible dans la fonction f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 * p1, struct t2 * p2)
{
      if (p1->m < 0)
            p2->m = -p2->m;
      return p1->m;
}
int g()
{
      union {
            struct t1 s1;
            struct t2 s2;
      } u;
      /* ... */
      return f(&u.s1, &u.s2);
}

Je n'ai trouvé aucune erreur ni avertissement lors de mon test.

Ma question est: pourquoi ce fragment n'est-il pas valide?

38
kangjianwei

L'exemple tente d'illustrer le paragraphe au préalable1 (c'est moi qui souligne):

6.5.2.3 ¶6

Une garantie spéciale est apportée afin de simplifier l'utilisation des unions: si une union contient plusieurs structures qui partagent une séquence initiale commune (voir ci-dessous), et si l'objet union contient actuellement une de ces structures, il est permis d'inspecter la partie initiale commune de n'importe lequel d'entre eux où une déclaration du type d'union complété est visible . Deux structures partagent une séquence initiale commune si les membres correspondants ont des types compatibles (et, pour les champs binaires, les mêmes largeurs) pour une séquence d'un ou plusieurs membres initiaux.

Puisque f est déclaré avant g, et en outre le type d'union sans nom est local à g, il n'y a aucun doute que le type d'union n'est pas visible dans f.

L'exemple ne montre pas comment u est initialisé, mais en supposant que le dernier écrit sur le membre est u.s2.m, la fonction a un comportement indéfini car elle inspecte p1->m sans que la garantie de séquence initiale commune soit en vigueur.

Il en va de même dans l'autre sens, si c'est u.s1.m qui a été écrit pour la dernière fois avant l'appel de fonction, que d'accéder à p2->m est un comportement indéfini.

Notez que f lui-même n'est pas invalide. C'est une définition de fonction parfaitement raisonnable. Le comportement indéfini découle du passage en lui &u.s1 et &u.s2 comme arguments. C'est ce qui cause un comportement indéfini.


1 - Je cite n157 , le projet standard C11. Mais la spécification doit être la même, sous réserve uniquement de déplacer un ou deux paragraphes vers le haut/bas.

33
StoryTeller

Voici la règle stricte d'alias en action: une hypothèse émise par le compilateur C (ou C++), est que le déréférencement de pointeurs vers des objets de différents types ne se référera jamais au même emplacement mémoire (c'est-à-dire qu'ils s'aliasent les uns les autres).

Cette fonction

int f(struct t1* p1, struct t2* p2);

suppose que p1 != p2 car ils pointent formellement vers différents types. Par conséquent, l'optimiseur peut supposer que p2->m = -p2->m; N'a aucun effet sur p1->m; il peut d'abord lire la valeur de p1->m dans un registre, le comparer avec 0, s'il compare moins de 0, puis faire p2->m = -p2->m; et finalement retourner la valeur de registre inchangée!

L'union ici est le seul moyen de créer p1 == p2 Au niveau binaire car tous les membres de l'union ont la même adresse.

Un autre exemple:

struct t1 { int m; };
struct t2 { int m; };

int f(struct t1* p1, struct t2* p2)
{
    if (p1->m < 0) p2->m = -p2->m;
    return p1->m;
}

int g()
{
    union {
        struct t1 s1;
        struct t2 s2;
    } u;
    u.s1.m = -1;
    return f(&u.s1, &u.s2);
}

Que doit renvoyer g? +1 Selon le bon sens (nous changeons -1 en +1 dans f). Mais si nous regardons la génération de l'assembly gcc avec l'optimisation -O1

f:
        cmp     DWORD PTR [rdi], 0
        js      .L3
.L2:
        mov     eax, DWORD PTR [rdi]
        ret
.L3:
        neg     DWORD PTR [rsi]
        jmp     .L2
g:
        mov     eax, 1
        ret

Jusqu'à présent, tout est comme excepté. Mais quand nous l'essayons avec -O2

f:
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        js      .L4
        ret
.L4:
        neg     DWORD PTR [rsi]
        ret
g:
        mov     eax, -1
        ret

La valeur de retour est maintenant un code dur -1

C'est parce que f au début met en cache la valeur de p1->m Dans le registre eax (mov eax, DWORD PTR [rdi]) Et ne le relit pas = après p2->m = -p2->m; (neg DWORD PTR [rsi]) - il renvoie eax inchangé.


union ici utilisé uniquement pour tous les membres de données non statiques d'un objet union ont la même adresse comme résultat &u.s1 == &u.s2.

si quelqu'un ne comprend pas le code assembleur, peut montrer dans c/c ++ comment l'aliasing strict affecte le code f:

int f(struct t1* p1, struct t2* p2)
{
    int a = p1->m;
    if (a < 0) p2->m = -p2->m;
    return a; 
}

cache du compilateur p1->m valeur dans la variable locale a (en fait dans le registre bien sûr) et retournez-la, malgré p2->m = -p2->m; changez p1->m. mais le compilateur suppose que la mémoire de p1 n'est pas affectée, car il suppose que p2 pointe vers une autre mémoire qui ne chevauche pas avec p1

ainsi, avec différents compilateurs et différents niveaux d'optimisation, le même code source peut renvoyer des valeurs différentes (-1 ou +1). donc et un comportement indéfini tel quel

26
RbMm

L'un des principaux objectifs de la règle de séquence initiale commune est de permettre aux fonctions de fonctionner de manière interchangeable sur de nombreuses structures similaires. Exiger que les compilateurs présument que toute fonction qui agit sur une structure pourrait changer le membre correspondant dans toute autre structure qui partage une séquence initiale commune, cependant, aurait altéré les optimisations utiles.

Bien que la plupart du code qui repose sur les garanties de séquence initiale commune utilise quelques modèles facilement reconnaissables, par exemple.

struct genericFoo {int size; short mode; };
struct fancyFoo {int size; short mode, biz, boz, baz; };
struct bigFoo {int size; short mode; char payload[5000]; };

union anyKindOfFoo {struct genericFoo genericFoo;
  struct fancyFoo fancyFoo;
  struct bigFoo bigFoo;};

...
if (readSharedMemberOfGenericFoo( myUnion->genericFoo ))
  accessThingAsFancyFoo( myUnion->fancyFoo );
return readSharedMemberOfGenericFoo( myUnion->genericFoo );

revisitant l'union entre les appels à des fonctions qui agissent sur différents membres du syndicat, les auteurs de la norme ont précisé que la visibilité du type d'union au sein de la fonction appelée devrait être le facteur déterminant pour savoir si les fonctions devraient reconnaître la possibilité qu'un accès à, par ex. le champ mode d'un FancyFoo peut affecter le champ mode d'un genericFoo. L'exigence d'avoir une union contenant tous les types de structures dont l'adresse pourrait être passée à readSharedMemberOfGeneric dans la même unité de compilation que cette fonction rend la règle de séquence initiale commune moins utile qu'elle ne le serait autrement, mais ferait au moins autoriser certains modèles comme celui ci-dessus utilisable.

Les auteurs de gcc et clang pensaient que le fait de traiter les déclarations d'union comme une indication que les types impliqués pourraient être impliqués dans des constructions comme celles ci-dessus serait cependant un obstacle pratique à l'optimisation, et ont pensé que, puisque la norme ne les oblige pas à prendre en charge de telles construit par d'autres moyens, ils ne les prendront tout simplement pas en charge. Par conséquent, la véritable exigence de code qui aurait besoin d'exploiter les garanties de séquence initiale commune de manière significative n'est pas de s'assurer qu'une déclaration de type d'union est visible, mais de s'assurer que clang et gcc sont invoqués avec le -fno-strict-aliasing drapeau. Inclure également une déclaration d'union visible lorsque cela n'est pas possible, mais ce n'est ni nécessaire ni suffisant pour garantir un comportement correct de gcc et clang.

3
supercat