Accès à un membre du syndicat inactif et comportement indéfini?

129

J'avais l'impression que l'accès à un union membre autre que le dernier ensemble est UB, mais je n'arrive pas à trouver une référence solide (autre que des réponses affirmant que c'est UB mais sans aucun support de la norme).

Alors, est-ce un comportement non défini?

Luchian Grigore
la source
3
C99 (et je crois aussi C ++ 11) autorise explicitement le poinçonnage de type avec les unions. Je pense donc que cela relève du comportement «défini par la mise en œuvre».
Mysticial
1
Je l'ai utilisé à plusieurs reprises pour convertir un int individuel en caractère. Donc, je sais vraiment que ce n'est pas indéfini. Je l'ai utilisé sur le compilateur Sun CC. Donc, cela peut encore dépendre du compilateur.
go4sri
42
@ go4sri: De toute évidence, vous ne savez pas ce que cela signifie pour un comportement indéfini. Le fait qu'il semble fonctionner pour vous dans certains cas ne contredit pas son caractère indéfini.
Benjamin Lindley
4
En relation: Objectif des unions en C et C ++
legends2k
4
@Mysticial, le billet de blog vers lequel vous créez un lien concerne très spécifiquement C99; cette question est balisée uniquement pour C ++.
davmac

Réponses:

131

La confusion est que C autorise explicitement le poinçonnage de type via une union, alors que C ++ () n'a pas une telle autorisation.

6.5.2.3 Structure et membres du syndicat

95) Si le membre utilisé pour lire le contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation d'objet de la valeur est réinterprétée comme une représentation d'objet dans le nouveau tapez comme décrit dans 6.2.6 (un processus parfois appelé «poinçonnage de type»). Cela pourrait être une représentation piège.

La situation avec C ++:

9.5 Unions [class.union]

Dans une union, au plus l'un des membres de données non statiques peut être actif à tout moment, c'est-à-dire que la valeur d'au plus l'un des membres de données non statiques peut être stockée dans une union à tout moment.

C ++ a plus tard un langage permettant l'utilisation d'unions contenant des structs avec des séquences initiales communes; cela ne permet cependant pas le poinçonnage de type.

Pour déterminer si la punition de type d'union est autorisé en C ++, nous devons chercher plus loin. Rappeler que est une référence normative pour C ++ 11 (et C99 a un langage similaire à C11 permettant l'union de type-punning):

3.9 Types [basic.types]

4 - La représentation objet d'un objet de type T est la suite de N objets char non signés repris par l'objet de type T, où N est égal à sizeof (T). La représentation de valeur d'un objet est l'ensemble de bits qui contiennent la valeur de type T.Pour les types trivialement copiables, la représentation de valeur est un ensemble de bits dans la représentation d'objet qui détermine une valeur, qui est un élément discret d'une implémentation. ensemble de valeurs défini. 42
42) L'intention est que le modèle de mémoire de C ++ soit compatible avec celui du langage de programmation ISO / CEI 9899 C.

Cela devient particulièrement intéressant quand on lit

3.8 Durée de vie de l'objet [basic.life]

La durée de vie d'un objet de type T commence lorsque: - le stockage avec l'alignement et la taille appropriés pour le type T est obtenu, et - si l'objet a une initialisation non triviale, son initialisation est terminée.

Ainsi, pour un type primitif (qui a ipso facto une initialisation triviale) contenu dans une union, la durée de vie de l'objet englobe au moins la durée de vie de l'union elle-même. Cela nous permet d'invoquer

3.9.2 Types composés [basic.compound]

Si un objet de type T se trouve à une adresse A, on dit qu'un pointeur de type cv T * dont la valeur est l'adresse A pointe vers cet objet, quelle que soit la manière dont la valeur a été obtenue.

En supposant que l'opération qui nous intéresse est le poinçonnage de type, c'est-à-dire qu'elle prend la valeur d'un membre syndical non actif, et que, conformément à ce qui précède, nous avons une référence valide à l'objet auquel ce membre fait référence, cette opération est lvalue-to -rvaleur conversion:

4.1 Conversion Lvalue-to-rvalue [conv.lval]

Une glvalue d'un type non-function, non-array Tpeut être convertie en prvalue. Si Test un type incomplet, un programme qui nécessite cette conversion est mal formé. Si l'objet auquel se réfère la valeur de glissement n'est pas un objet de type Tet n'est pas un objet d'un type dérivé de T, ou si l'objet n'est pas initialisé, un programme qui nécessite cette conversion a un comportement non défini.

La question est alors de savoir si un objet qui est un membre syndical non actif est initialisé par stockage sur le membre syndical actif. Pour autant que je sache, ce n'est pas le cas et donc si:

  • une union est copiée dans le charstockage de la baie et inversement (3.9: 2), ou
  • une union est copiée par octet dans une autre union du même type (3.9: 3), ou
  • une union est accessible au-delà des frontières linguistiques par un élément de programme conforme à ISO / CEI 9899 (pour autant que cela soit défini) (3.9: 4 note 42), alors

l'accès à une union par un membre non actif est défini et est défini pour suivre la représentation de l'objet et de la valeur, l'accès sans l'une des interpositions ci-dessus est un comportement indéfini. Cela a des implications pour les optimisations autorisées à être effectuées sur un tel programme, car l'implémentation peut bien sûr supposer qu'un comportement indéfini ne se produit pas.

Autrement dit, bien que nous puissions légitimement former une lvalue à un membre syndical non actif (c'est pourquoi l'attribution à un membre non actif sans construction est acceptable), elle est considérée comme non initialisée.

ecatmur
la source
5
3.8 / 1 indique que la durée de vie d'un objet se termine lorsque son stockage est réutilisé. Cela m'indique qu'un membre non actif de la vie d'un syndicat a pris fin parce que son stockage a été réutilisé pour le membre actif. Cela signifierait que vous êtes limité dans la façon dont vous utilisez le membre (3.8 / 6).
bames53
2
Selon cette interprétation, chaque bit de mémoire contient simultanément des objets de tous types qui sont trivialement initialisables et ont un alignement approprié ... Ainsi, la durée de vie de tout type non trivialement initialisable se termine immédiatement lorsque son stockage est réutilisé pour tous ces autres types ( et ne pas redémarrer car ils ne sont pas trivialement initiables)?
bames53
3
Le libellé 4.1 est complètement et complètement cassé et a depuis été réécrit. Il interdisait toutes sortes de choses parfaitement valides: il interdisait les memcpyimplémentations personnalisées (accès aux objets à l'aide de unsigned charlvalues), il interdisait les accès à *pafter int *p = 0; const int *const *pp = &p;(même si la conversion implicite de int**à const int*const*est valide), il interdisait même l'accès caprès struct S s; const S &c = s;. Numéro 616 du CWG . Le nouveau libellé le permet-il? Il y a aussi [basic.lval].
2
@Omnifarious: Cela aurait du sens, mais il faudrait également clarifier (et le standard C doit également clarifier, btw) ce que &signifie l'opérateur unaire lorsqu'il est appliqué à un membre du syndicat. Je pense que le pointeur résultant devrait être utilisable pour accéder au membre au moins jusqu'à la prochaine utilisation directe ou indirecte de tout autre membre lvalue, mais dans gcc, le pointeur n'est pas utilisable même si longtemps, ce qui soulève la question de savoir quoi l' &opérateur est censé vouloir dire.
supercat
4
Une question concernant "Rappelez-vous que c99 est une référence normative pour C ++ 11" N'est-ce pas seulement pertinent, là où le standard c ++ fait explicitement référence au standard C (par exemple pour les fonctions de la bibliothèque c)?
MikeMB
28

La norme C ++ 11 le dit de cette façon

9.5 Syndicats

Dans une union, au plus l'un des membres de données non statiques peut être actif à tout moment, c'est-à-dire que la valeur d'au plus l'un des membres de données non statiques peut être stockée dans une union à tout moment.

Si une seule valeur est stockée, comment pouvez-vous en lire une autre? Ce n'est tout simplement pas là.


La documentation gcc le répertorie sous Comportement défini par l'implémentation

  • L'accès à un membre d'un objet union s'effectue à l'aide d'un membre d'un type différent (C90 6.3.2.3).

Les octets pertinents de la représentation de l'objet sont traités comme un objet du type utilisé pour l'accès. Voir Punition de type. Cela peut être une représentation piège.

indiquant que cela n'est pas exigé par la norme C.


2016-01-05: Grâce aux commentaires, j'ai été lié au rapport de défaut C99 n ° 283 qui ajoute un texte similaire en tant que note de bas de page au document standard C:

78a) Si le membre utilisé pour accéder au contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation d'objet de la valeur est réinterprétée comme une représentation d'objet dans le nouveau tapez comme décrit en 6.2.6 (un processus parfois appelé "poinçonnage de type"). Cela pourrait être une représentation piège.

Je ne sais pas si cela clarifie beaucoup, étant donné qu'une note de bas de page n'est pas normative pour la norme.

Bo Persson
la source
10
@LuchianGrigore: UB n'est pas ce que la norme dit être UB, c'est plutôt ce que la norme ne décrit pas comment cela devrait fonctionner. C'est exactement le cas. La norme décrit-elle ce qui se passe? Dit-il que sa mise en œuvre est définie? Non et non. C'est donc UB. De plus, en ce qui concerne l'argument "membres partagent la même adresse mémoire", vous devrez vous référer aux règles d'aliasing, ce qui vous ramènera à UB.
Yakov Galka
5
@Luchian: Ce que signifie actif est assez clair, "c'est-à-dire que la valeur d'au plus l'un des membres de données non statiques peut être stockée dans une union à tout moment."
Benjamin Lindley
5
@LuchianGrigore: Oui, il y en a. Il existe un nombre infini de cas que la norme ne traite pas (et ne peut pas) traiter. (C ++ est une VM complète de Turing donc elle est incomplète.) Et alors? Cela explique ce que signifie "actif", reportez-vous à la citation ci-dessus, après "c'est-à-dire".
Yakov Galka
8
@LuchianGrigore: L'omission de la définition explicite du comportement est également un comportement non défini non considéré, selon la section des définitions.
jxh
5
@Claudiu C'est UB pour une raison différente - cela viole l'alias strict.
Mysticial
18

Je pense que le plus proche de la norme vient de dire que son comportement indéfini est celui où il définit le comportement d'une union contenant une séquence initiale commune (C99, §6.5.2.3 / 5):

Une garantie particulière est faite 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 l'une de ces structures, il est permis d'inspecter le commun partie initiale de l'un d'entre eux partout où une déclaration du type complet de l'union est visible. Deux structures partagent une séquence initiale commune si les membres correspondants ont des types compatibles (et, pour les champs de bits, les mêmes largeurs) pour une séquence d'un ou plusieurs membres initiaux.

C ++ 11 donne des exigences / autorisations similaires au §9.2 / 19:

Si une union de mise en page standard contient deux ou plusieurs structures de mise en page standard qui partagent une séquence initiale commune, et si l'objet union de mise en page standard contient actuellement l'une de ces structures de mise en page standard, il est permis d'inspecter la partie initiale commune de tout d'eux. Deux structures de mise en page standard partagent une séquence initiale commune si les membres correspondants ont des types compatibles avec la mise en page et qu'aucun membre n'est un champ de bits ou les deux sont des champs de bits de la même largeur pour une séquence d'un ou plusieurs membres initiaux.

Bien que ni l'un ni l'autre ne le déclare directement, ces deux éléments impliquent fortement que "inspecter" (lire) un membre n'est "autorisé" que si 1) il fait (partie de) le membre le plus récemment écrit, ou 2) fait partie d'une initiale commune séquence.

Ce n'est pas une déclaration directe que faire autrement est un comportement indéfini, mais c'est le plus proche dont je suis conscient.

Jerry Coffin
la source
Pour que cela soit complet, vous devez savoir quels sont les «types compatibles avec la mise en page» pour C ++, ou les «types compatibles» pour C.
Michael Anderson
2
@MichaelAnderson: Oui et non. Vous devez vous en occuper lorsque / si vous voulez être certain que quelque chose relève de cette exception - mais la vraie question ici est de savoir si quelque chose qui tombe clairement en dehors de l'exception donne vraiment UB. Je pense que c'est assez fortement sous-entendu ici pour que l'intention soit claire, mais je ne pense pas qu'elle soit jamais énoncée directement.
Jerry Coffin
Cette chose de "séquence initiale commune" aurait pu juste avoir sauvé 2 ou 3 de mes projets de la corbeille de réécriture. J'étais furieux quand j'ai lu pour la première fois que la plupart des utilisations des jeux de mots n'étaient unionpas définies, car un blog en particulier m'avait donné l'impression que c'était correct, et j'avais construit plusieurs grandes structures et projets autour de lui. Maintenant, je pense que ça ira peut-être après tout, puisque mes unions contiennent des classes ayant les mêmes types à l'avant
underscore_d
@JerryCoffin, je pense que vous faisiez allusion à la même question que moi: que se passe-t-il si notre unioncontient par exemple un uint8_tet un class Something { uint8_t myByte; [...] };- je suppose que cette disposition s'appliquerait également ici, mais elle est formulée très délibérément pour n'autoriser que l' structart. Heureusement, j'utilise déjà ceux-ci au lieu des primitives brutes: O
underscore_d
@underscore_d: Le standard C couvre au moins en quelque sorte cette question: "Un pointeur vers un objet structure, convenablement converti, pointe vers son membre initial (ou si ce membre est un bit-field, alors vers l'unité dans laquelle il réside) , et vice versa."
Jerry Coffin du
12

Quelque chose qui n'est pas encore mentionné par les réponses disponibles est la note de bas de page 37 au paragraphe 21 de la section 6.2.5:

Notez que le type d'agrégat n'inclut pas le type d'union car un objet de type union ne peut contenir qu'un seul membre à la fois.

Cette exigence semble clairement impliquer que vous ne devez pas écrire dans un membre et lire dans un autre. Dans ce cas, il peut s'agir d'un comportement indéfini par manque de spécification.

mpu
la source
De nombreuses implémentations documentent leurs formats de stockage et leurs règles de disposition. Une telle spécification impliquerait dans de nombreux cas quel serait l'effet de la lecture du stockage d'un type et de l'écriture comme un autre en l'absence de règles disant que les compilateurs n'ont pas à utiliser réellement leur format de stockage défini sauf lorsque les choses sont lues et écrites à l'aide de pointeurs. d'un type de caractère.
supercat le
-3

J'explique bien cela avec un exemple.
supposons que nous ayons l'union suivante:

union A{
   int x;
   short y[2];
};

Je suppose bien que cela sizeof(int)donne 4, et cela sizeof(short)donne 2.
quand vous écrivezunion A a = {10} bien, créez une nouvelle var de type A en lui mettant la valeur 10.

votre mémoire devrait ressembler à ça: (rappelez-vous que tous les membres du syndicat ont le même emplacement)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

comme vous pouvez le voir, la valeur de ax est 10, la valeur de ay 1 est 10 et la valeur de ay [0] est 0.

maintenant, que se passe-t-il si je fais ça?

a.y[0] = 37;

notre mémoire ressemblera à ceci:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

cela transformera la valeur de ax à 2424842 (en décimal).

maintenant, si votre union a un flottant, ou double, votre carte mémoire sera bien plus en désordre, à cause de la façon dont vous stockez les nombres exacts. plus d'informations que vous pourriez obtenir ici .

elyashiv
la source
18
:) Ce n'est pas ce que j'ai demandé. Je sais ce qui se passe en interne. Je sais que ça marche. J'ai demandé si c'était dans la norme.
Luchian Grigore