But des unions en C et C ++

J’ai utilisé les syndicats plus tôt, confortablement; aujourd’hui, j’ai été alarmé quand j’ai lu ce post et en est venu à savoir que ce code

union ARGB { uint32_t colour; struct componentsTag { uint8_t b; uint8_t g; uint8_t r; uint8_t a; } components; } pixel; pixel.colour = 0xff040201; // ARGB::colour is the active member from now on // somewhere down the line, without any edit to pixel if(pixel.components.a) // accessing the non-active member ARGB::components 

est en fait un comportement indéfini, c’est-à-dire que la lecture d’un membre de l’union autre que celui récemment écrit entraîne un comportement indéfini. Si ce n’est pas l’usage prévu des syndicats, qu’est-ce que c’est? Quelqu’un peut-il l’expliquer de manière élaborée?

Mettre à jour:

Je voulais clarifier quelques points avec le recul.

  • La réponse à la question n’est pas la même pour C et C ++; mon plus jeune ignorant l’a étiqueté comme C et C ++.
  • Après avoir parcouru le standard C ++ 11, je ne pouvais pas dire avec certitude qu’il appelle l’access / l’inspection d’un membre d’union non actif, qu’il est indéfini / non spécifié / défini par l’implémentation. Tout ce que j’ai pu trouver était le §9.5 / 1:

    Si une union de présentation standard contient plusieurs structures de disposition standard partageant une séquence initiale commune et si un object de ce type d’union de présentation standard contient l’une des structures de disposition standard, il est possible d’inspecter la séquence initiale commune de tout des membres de structure standard-layout. §9.2 / 19: Deux structures de présentation standard partagent une séquence initiale commune si les membres correspondants ont des types compatibles avec la mise en page et que ni l’un ni l’autre ne soit un champ de bits ou les deux sont des champs de même largeur pour une séquence membres.

  • En C, ( C99 TC3 – DR 283 et suivants), il est légal de le faire ( merci à Pascal Cuoq de l’avoir évoqué). Cependant, si vous tentez de le faire, vous risquez toujours d’avoir un comportement indéfini si la valeur lue arrive à être invalide (appelée “représentation par piège”) pour le type lu. Sinon, la valeur lue est définie par l’implémentation.
  • C89 / 90 a appelé cela sous un comportement non spécifié (annexe J) et le livre de K & R indique que sa mise en œuvre est définie. Citation de K & R:

    C’est le but d’une union – une variable unique qui peut légitimement détenir l’un de plusieurs types. […] tant que l’utilisation est cohérente: le type récupéré doit être le type le plus récemment stocké. Il est de la responsabilité du programmeur de savoir quel type est actuellement stocké dans une union; les résultats dépendent de l’implémentation si quelque chose est stocké sous un type et extrait sous un autre.

  • Extrait de TC ++ PL de Stroustrup

    L’utilisation des unions peut être essentielle pour assurer la compatibilité des données […] parfois mal utilisées pour la “conversion de type “.

Surtout, cette question (dont le titre rest inchangé depuis ma demande) a été posée avec l’intention de comprendre le but des unions ET non pas ce que la norme autorise . Par exemple, le standard C ++ autorise l’ utilisation de l’inheritance pour la réutilisation du code. ce n’était pas le but ou l’intention originale d’introduire l’inheritance en tant que fonctionnalité du langage C ++ . C’est la raison pour laquelle la réponse d’Andrey rest la même.

Le but des syndicats est plutôt évident, mais pour une raison quelconque, les gens le manquent assez souvent.

L’union a pour but d’économiser de la mémoire en utilisant la même région de mémoire pour stocker différents objects à des moments différents. C’est tout.

C’est comme une chambre dans un hôtel. Différentes personnes y vivent pendant des périodes qui ne se chevauchent pas. Ces personnes ne se rencontrent jamais et ne savent généralement rien les unes des autres. En gérant correctement le partage du temps des chambres (en veillant à ce que des personnes différentes ne soient pas affectées à une même pièce), un hôtel relativement petit peut fournir un hébergement à un nombre relativement important de personnes. sont pour.

C’est exactement ce que fait l’union. Si vous savez que plusieurs objects de votre programme contiennent des valeurs avec des valeurs de durée de vie qui ne se chevauchent pas, vous pouvez “fusionner” ces objects dans une union et économiser ainsi de la mémoire. Tout comme une chambre d’hôtel a au plus un locataire «actif» à chaque instant, un syndicat a au plus un membre «actif» à chaque moment du programme. Seul le membre “actif” peut être lu. En écrivant dans un autre membre, vous basculez le statut “actif” vers cet autre membre.

Pour une raison quelconque, l’objective initial de l’union a été “complètement dépassé” avec quelque chose de complètement différent: écrire un membre d’un syndicat et l’inspecter par un autre membre. Ce type de réinterprétation de la mémoire (alias “type punning”) n’est pas une utilisation valable des unions. Elle conduit généralement à un comportement indéfini qui est décrit comme produisant un comportement défini par la mise en œuvre dans C89 / 90.

EDIT: L’ utilisation des unions pour les types de punition (c’est-à-dire écrire un membre et en lire un autre) a été définie plus précisément dans l’un des correctifs techniques de la norme C99 (voir DR # 257 et DR # 283 ). Cependant, gardez à l’esprit que cela ne vous protège pas formellement du fait que vous tentez de lire une représentation d’interruption.

Vous pouvez utiliser des unions pour créer des structures comme celle-ci, qui contient un champ qui nous indique quel composant de l’union est réellement utilisé:

 struct VAROBJECT { enum o_t { Int, Double, Ssortingng } objectType; union { int intValue; double dblValue; char *strValue; } value; } object; 

Le comportement est indéfini du sharepoint vue du langage. Considérez que différentes plates-formes peuvent avoir des contraintes différentes en matière d’alignement de la mémoire et d’endianness. Le code dans un big endian versus un petit ordinateur endian mettra à jour les valeurs dans la structure différemment. Fixer le comportement dans le langage exigerait que toutes les implémentations utilisent le même endianness (et les contraintes d’alignement de la mémoire …) limitant l’utilisation.

Si vous utilisez C ++ (vous utilisez deux balises) et que vous vous souciez vraiment de la portabilité, vous pouvez simplement utiliser la structure et fournir un setter prenant uint32_t et définissant les champs de manière appropriée via les opérations bitmask. La même chose peut être faite en C avec une fonction.

Edit : Je m’attendais à ce qu’AProgrammer écrive une réponse pour voter et fermer celle-ci. Comme certains commentaires l’ont souligné, l’endianness est traité dans d’autres parties du standard en laissant chaque implémentation décider de ce qu’il faut faire, et l’alignement et le remplissage peuvent également être traités différemment. Maintenant, les règles ssortingctes d’alias auxquelles AProgrammer fait référence implicitement sont un point important ici. Le compilateur est autorisé à formuler des hypothèses sur la modification (ou l’absence de modification) des variables. Dans le cas de l’union, le compilateur pourrait réorganiser les instructions et déplacer la lecture de chaque composante de couleur sur l’écriture dans la variable de couleur.

L’utilisation la plus courante de l’ union je rencontre régulièrement est l’ aliasing .

Considérer ce qui suit:

 union Vector3f { struct{ float x,y,z ; } ; float elts[3]; } 

Qu’est-ce que cela fait? Il permet un access net et net à un Vector3f vec; Les membres par l’un ou l’autre nom:

 vec.x=vec.y=vec.z=1.f ; 

ou par access entier au tableau

 for( int i = 0 ; i < 3 ; i++ ) vec.elts[i]=1.f; 

Dans certains cas, l'access par nom est la chose la plus claire que vous puissiez faire. Dans d'autres cas, en particulier lorsque l'axe est choisi par programme, la chose la plus facile à faire est d'accéder à l'axe par index numérique - 0 pour x, 1 pour y et 2 pour z.

Comme vous le dites, il s’agit d’un comportement ssortingctement indéfini, même s’il fonctionnera sur de nombreuses plates-formes. La véritable raison d’utiliser des unions est de créer des enregistrements de variantes.

 union A { int i; double d; }; A a[10]; // records in "a" can be either ints or doubles a[0].i = 42; a[1].d = 1.23; 

Bien sûr, vous avez également besoin d’une sorte de discriminateur pour dire ce que la variante contient réellement. Et notez que dans le C ++, les unions ne sont pas très utiles car elles ne peuvent contenir que des types de POD – ceux qui sont sans constructeurs et sans destructeurs.

En C, c’était une bonne façon d’implémenter quelque chose comme une variante.

 enum possibleTypes{ eInt, eDouble, eChar } struct Value{ union Value { int iVal_; double dval; char cVal; } value_; possibleTypes discriminator_; } switch(val.discriminator_) { case eInt: val.value_.iVal_; break; 

Dans les temps de mémoire, cette structure utilise moins de mémoire qu’une structure qui contient tous les membres.

Au fait, C fournit

  typedef struct { unsigned int mantissa_low:32; //mantissa unsigned int mantissa_high:20; unsigned int exponent:11; //exponent unsigned int sign:1; } realVal; 

pour accéder aux valeurs de bit.

En C ++, Boost Variant implémente une version sécurisée de l’union, conçue pour empêcher autant que possible un comportement indéfini.

Ses performances sont identiques à la construction enum + union (stack allouée aussi, etc.) mais il utilise une liste de types de types au lieu de l’ enum 🙂

Bien que ce soit un comportement ssortingctement indéfini, dans la pratique, cela fonctionnera à peu près avec n’importe quel compilateur. C’est un paradigme si largement utilisé que tout compilateur qui se respecte devra faire “la bonne chose” dans des cas comme celui-ci. Il est certainement préférable de choisir le type-punning, qui peut générer du code cassé avec certains compilateurs.

Techniquement, ce n’est pas défini, mais en réalité, la plupart des compilateurs (tous?) Le traitent exactement de la même manière que d’utiliser un reinterpret_cast d’un type à l’autre, dont le résultat est défini par l’implémentation. Je ne perdrais pas mon sumil avec votre code actuel.

Pour un autre exemple d’utilisation réelle des unions, le framework CORBA sérialise les objects en utilisant l’approche d’union balisée. Toutes les classes définies par l’utilisateur sont membres d’une union (énorme) et un identificateur d’entier indique au responsable de la démonstration comment interpréter l’union.

Le comportement peut être indéfini, mais cela signifie simplement qu’il n’y a pas de “standard”. Tous les compilateurs décents proposent des #pragmas pour contrôler le compactage et l’alignement, mais peuvent avoir des valeurs par défaut différentes. Les valeurs par défaut changeront également en fonction des parameters d’optimisation utilisés.

En outre, les syndicats ne servent pas uniquement à économiser de l’espace. Ils peuvent aider les compilateurs modernes avec un type de punition. Si vous reinterpret_cast<> tout le compilateur ne peut pas faire de suppositions sur ce que vous faites. Il se peut qu’il doive jeter ce qu’il sait sur votre type et recommencer (forçant une réécriture en mémoire, ce qui est très inefficace par rapport à la vitesse d’horloge du processeur).

D’autres ont mentionné les différences d’architecture (little-big endian).

J’ai lu le problème que, puisque la mémoire des variables est partagée, alors en écrivant à l’une, les autres changent et, selon leur type, la valeur pourrait être dénuée de sens.

par exemple. union {float f; int i } X;

Écrire dans xi n’aurait aucun sens si vous lisiez à partir de xf – à moins que ce ne soit ce que vous vouliez pour examiner les composantes signe, exposant ou mantisse du flottant.

Je pense qu’il y a aussi un problème d’alignement: Si certaines variables doivent être alignées sur des mots, vous risquez de ne pas obtenir le résultat attendu.

par exemple. union {char c [4]; int i } X;

Si, sur une machine, un caractère devait être aligné sur un mot, c [0] et c [1] partageraient le stockage avec i mais pas avec c [2] et c [3].

Dans le langage C tel qu’il a été documenté en 1974, tous les membres de la structure partageaient un espace de noms commun, et la signification de “ptr-> member” était d’append le déplacement du membre à “ptr” et d’accéder à l’adresse résultante en utilisant le type du membre. Cette conception a permis d’utiliser le même ptr avec des noms de membres issus de définitions de structures différentes mais avec le même décalage; les programmeurs ont utilisé cette capacité à diverses fins.

Lorsque les membres de la structure se voyaient atsortingbuer leurs propres espaces de noms, il devenait impossible de déclarer deux membres de la structure avec le même déplacement. L’ajout d’union au langage a permis d’obtenir la même sémantique que celle disponible dans les versions antérieures du langage (bien que l’impossibilité d’exporter des noms dans un contexte englobant puisse nécessiter l’utilisation d’un membre find / replace pour remplacer foo-> dans foo-> type1.member). L’important n’était pas tant que les personnes qui ajoutent des syndicats aient en tête une utilisation ciblée particulière, mais plutôt qu’elles fournissent un moyen par lequel les programmeurs qui se sont fiés à la sémantique antérieure, pour quelque raison que ce soit , soient encore capables d’atteindre même sémantique même s’ils doivent utiliser une syntaxe différente pour le faire.

Vous pouvez utiliser une union pour deux raisons principales:

  1. Un moyen pratique d’accéder aux mêmes données de différentes manières, comme dans votre exemple
  2. Une manière d’économiser de l’espace lorsqu’il existe différents membres de données dont un seul peut être “actif”

1 S’agit-il plutôt d’un hack de style C pour raccourcir le code d’écriture sur la base de savoir comment fonctionne l’architecture de mémoire du système cible. Comme déjà dit, vous pouvez normalement vous en tirer si vous ne ciblez pas beaucoup de plates-formes différentes. Je pense que certains compilateurs peuvent vous laisser utiliser les directives d’emballage (je sais qu’ils le font sur les structures)?

Un bon exemple de 2. peut être trouvé dans le type VARIANT largement utilisé dans COM.