J'ai utilisé les syndicats plus tôt confortablement; aujourd'hui, j'ai été alarmé en lisant cet article et j'ai appris 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 écrit récemment conduit à un comportement indéfini. Si ce n'est pas l'usage prévu des syndicats, c'est quoi? Quelqu'un peut-il l'expliquer de manière détaillée?
Mettre à jour:
Je voulais clarifier certaines choses avec le recul.
- La réponse à la question n'est pas la même pour C et C ++; mon jeune ignorant l'a étiqueté à la fois comme C et C ++.
- Après avoir parcouru la norme C ++ 11, je ne pouvais pas dire de façon concluante qu'elle appelle l'accès / l'inspection d'un membre d'union non actif est indéfini / non spécifié / défini par la mise en œuvre. Tout ce que j'ai pu trouver était le §9.5 / 1:
Si une union de mise en page standard contient plusieurs structures de mise en page standard qui partagent une séquence initiale commune, et si un objet de ce type d'union de mise en page standard contient l'une des structures de mise en page standard, il est autorisé d'inspecter la séquence initiale commune de tout des membres de structure de mise en page standard. §9.2 / 19: 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 des membres n'est un champ de bits ou les deux sont des champs de bits de même largeur pour une séquence d'une ou plusieurs initiales membres.
- En C (à partir de C99 TC3 - DR 283 ), il est légal de le faire ( merci à Pascal Cuoq de l'avoir soulevé). Cependant, tenter de le faire peut toujours conduire à un comportement indéfini , si la valeur lue s'avère non valide (appelée "représentation d'interruption") pour le type par lequel elle est lue. 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 dit 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 contenir 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 garder une trace du type qui 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 du TC ++ PL de Stroustrup (accent sur le mien)
L'utilisation d'unions peut être essentielle pour la compatibilité des données [...] parfois mal utilisées pour la "conversion de type ".
Surtout, cette question (dont le titre reste inchangé depuis ma demande) a été posée dans le but de comprendre le but des unions ET non sur ce que la norme autorise . ce n'était pas le but ou l'intention initiale d'introduire l'héritage en tant que fonctionnalité du langage C ++ . C'est la raison pour laquelle la réponse d'Andrey reste celle acceptée.
la source
b, g, r,
eta
peut ne pas être contiguë, et donc ne pas correspondre à la disposition d'unuint32_t
. Cela s'ajoute aux problèmes d'endianisme que d'autres ont signalés.scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1
...vraiment? vous citez une note d' exception , pas le point principal au début du paragraphe : "Dans une union, au plus l'un des membres de données non statiques peut être actif à tout moment, c'est-à-dire la valeur d'au plus l'un des les membres de données non statiques peuvent être stockés dans une union à tout moment. " - et jusqu'à p4: "En général, il faut utiliser des appels de destructeur explicites et placer de nouveaux opérateurs pour changer le membre actif d'une union "Réponses:
Le but des syndicats est assez é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 objets à différents moments. 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 gens ne se rencontrent jamais et ne se connaissent généralement pas. En gérant correctement le partage du temps des chambres (c'est-à-dire en veillant à ce que différentes personnes ne soient pas affectées à une chambre en même temps), un hôtel relativement petit peut fournir un hébergement à un nombre relativement important de personnes, ce que les hôtels sont pour.
C'est exactement ce que fait l'union. Si vous savez que plusieurs objets de votre programme contiennent des valeurs avec des durées de vie sans chevauchement, vous pouvez "fusionner" ces objets en une union et ainsi économiser de la mémoire. Tout comme une chambre d'hôtel compte au plus un locataire «actif» à chaque instant, un syndicat compte au plus un membre «actif» à chaque instant du programme. Seul le membre "actif" peut être lu. En écrivant dans un autre membre, vous passez le statut "actif" à cet autre membre.
Pour une raison quelconque, cet objectif initial du syndicat a été «outrepassé» par quelque chose de complètement différent: écrire un membre d'un syndicat et l'inspecter par le biais d'un autre membre. Ce type de réinterprétation de la mémoire (alias "type punning") n'est
pas une utilisation valide des unions. Cela conduit généralement à un comportement indéfinidécrit comme produisant un comportement défini par l'implémentation dans C89 / 90.EDIT: L' utilisation des syndicats à des fins de punition de type (c'est-à-dire écrire un membre puis en lire un autre) a reçu une définition plus détaillée dans l'un des rectificatifs techniques de la norme C99 (voir DR # 257 et DR # 283 ). Cependant, gardez à l'esprit que formellement, cela ne vous protège pas contre un comportement indéfini en essayant de lire une représentation d'interruption.
la source
<time.h>
Windows et Unix. Le rejeter comme «non valide» et «non défini» n'est pas vraiment suffisant si je vais être appelé à comprendre du code qui fonctionne exactement de cette façon.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é:
la source
int
ouchar*
pour 10 objets []; dans ce cas, je peux réellement déclarer des structures distinctes pour chaque type de données au lieu de VAROBJECT? Cela ne réduirait-il pas l'encombrement et n'utiliserait-il pas moins d'espace?Le comportement n'est pas défini du point de vue de la langue. Considérez que différentes plates-formes peuvent avoir différentes contraintes d'alignement de mémoire et d'endianité. Le code d'un gros endian par rapport à une petite machine endian mettra à jour les valeurs de la structure différemment. La correction du comportement dans le langage nécessiterait que toutes les implémentations utilisent la même endianité (et contraintes d'alignement 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 qui prend
uint32_t
et définit les champs de manière appropriée via les opérations de masque de bits. 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'endianité est traitée dans d'autres parties de la norme en laissant chaque implémentation décider quoi faire, et l'alignement et le remplissage peuvent également être traités différemment. Maintenant, les règles strictes d'aliasing auxquelles AProgrammer se réfère implicitement sont un point important ici. Le compilateur est autorisé à faire des hypothèses sur la modification (ou l'absence de modification) des variables. Dans le cas de l'union, le compilateur peut réorganiser les instructions et déplacer la lecture de chaque composant de couleur sur l'écriture dans la variable de couleur.
la source
L' utilisation la plus courante de
union
je rencontre régulièrement est l' aliasing .Considérer ce qui suit:
Qu'est-ce que cela fait? Il permet un accès propre et soigné aux
Vector3f vec;
membres de a par l'un ou l'autre nom:ou par accès entier dans le tableau
Dans certains cas, l'accès 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 simple à faire est d'accéder à l'axe par index numérique - 0 pour x, 1 pour y et 2 pour z.
la source
type-punning
ce qui est également mentionné dans la question. L'exemple de la question montre également un exemple similaire.Comme vous le dites, il s'agit d'un comportement strictement indéfini, bien qu'il "fonctionne" sur de nombreuses plates-formes. La vraie raison de l'utilisation des unions est de créer des enregistrements de variantes.
Bien sûr, vous avez également besoin d'une sorte de discriminateur pour dire ce que contient réellement la variante. Et notez qu'en C ++, les unions ne sont pas très utiles car elles ne peuvent contenir que des types POD - en réalité ceux sans constructeurs et destructeurs.
la source
En C, c'était une bonne façon d'implémenter quelque chose comme une variante.
En période de petite mémoire, cette structure utilise moins de mémoire qu'une structure qui a tous les membres.
Soit dit en passant C
pour accéder aux valeurs des bits.
la source
Bien qu'il s'agisse d'un comportement strictement indéfini, en pratique, cela fonctionnera avec à peu près n'importe quel compilateur. C'est un paradigme tellement largement utilisé que tout compilateur qui se respecte devra faire «la bonne chose» dans des cas comme celui-ci. C'est certainement préférable à la punition de type, qui peut bien générer du code cassé avec certains compilateurs.
la source
En C ++, Boost Variant implémente une version sûre de l'union, conçue pour éviter autant que possible les comportements non définis.
Ses performances sont identiques à la
enum + union
construction (pile allouée aussi etc) mais il utilise une liste de modèles de types au lieu deenum
:)la source
Le comportement n'est peut-être pas défini, mais cela signifie simplement qu'il n'y a pas de "standard". Tous les compilateurs décents proposent #pragmas pour contrôler l'empaquetage et l'alignement, mais peuvent avoir des valeurs par défaut différentes. Les valeurs par défaut changeront également en fonction des paramètres d'optimisation utilisés.
De plus, les syndicats ne servent pas uniquement à économiser de l'espace. Ils peuvent aider les compilateurs modernes avec le type punning. Si vous
reinterpret_cast<>
tout le compilateur ne peut pas faire d'hypothèses sur ce que vous faites. Il devra peut-être jeter ce qu'il sait de votre type et recommencer (forcer une écriture en mémoire, ce qui est très inefficace de nos jours par rapport à la vitesse d'horloge du processeur).la source
Techniquement, il n'est pas défini, mais en réalité la plupart (tous?) Des compilateurs le traitent exactement de la même manière que l'utilisation
reinterpret_cast
d'un type à l'autre, dont le résultat est défini par l'implémentation. Je ne perdrais pas le sommeil sur votre code actuel.la source
Pour un autre exemple de l'utilisation réelle des unions, le cadre CORBA sérialise les objets en utilisant l'approche d'union étiquetée. Toutes les classes définies par l'utilisateur sont membres d'une (énorme) union, et un identifiant entier indique au demarshaller comment interpréter l'union.
la source
D'autres ont mentionné les différences d'architecture (petit - grand endian).
J'ai lu le problème que puisque la mémoire des variables est partagée, puis en écrivant sur l'une, les autres changent et, selon leur type, la valeur peut être dénuée de sens.
par exemple. union {float f; int i; } X;
Écrire à xi n'aurait aucun sens si vous lisez ensuite à partir de xf - à moins que ce soit ce que vous vouliez afin de regarder les composants signe, exposant ou mantisse du flotteur.
Je pense qu'il y a aussi un problème d'alignement: si certaines variables doivent être alignées sur un mot, vous n'obtiendrez peut-être pas le résultat attendu.
par exemple. union {char c [4]; int i; } X;
Si, hypothétiquement, sur une machine, un caractère devait être aligné sur un mot, alors c [0] et c [1] partageraient le stockage avec i mais pas c [2] et c [3].
la source
memcpy()
de l'un à l'autre. Certains systèmes peuvent aligner de manière spéculative leschar[]
allocations qui se produisent en dehors des structures / unions pour cela et pour d'autres raisons. Dans l'exemple actuel, l'hypothèse quii
chevauchera tous les éléments dec[]
n'est pas portable, mais c'est parce qu'il n'y a aucune garantiesizeof(int)==4
.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-> membre" a été définie comme l'ajout du déplacement du membre à "ptr" et l'accès à l'adresse résultante à l'aide du type de membre. Cette conception a permis d'utiliser le même ptr avec des noms de membres issus de définitions de structure différentes mais avec le même décalage; les programmeurs ont utilisé cette capacité à diverses fins.
Lorsque les membres de la structure se sont vu attribuer leurs propres espaces de noms, il est devenu impossible de déclarer deux membres de la structure avec le même déplacement. L'ajout d'unions à la langue a permis d'obtenir la même sémantique qui était disponible dans les versions antérieures de la langue (bien que l'impossibilité d'exporter les noms dans un contexte englobant ait pu nécessiter l'utilisation d'une fonction de recherche / remplacement pour remplacer foo-> member dans foo-> type1.member). Ce qui était important, ce n’était pas tant que les personnes qui ont ajouté des syndicats aient à l’esprit un objectif particulier, mais plutôt qu’elles fournissent un moyen par lequel les programmeurs qui s’étaient appuyés sur la sémantique précédente, à quelque fin que ce soit , devraient pouvoir atteindre même sémantique même s'ils devaient utiliser une syntaxe différente pour le faire.
la source
Vous pouvez utiliser une union pour deux raisons principales:
1 Est vraiment plus un hack de style C pour raccourcir l'écriture de code sur la base que vous savez 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 crois que certains compilateurs peuvent également vous permettre d'utiliser des 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.
la source
Comme d'autres l'ont mentionné, les unions combinées avec des énumérations et enveloppées dans des structures peuvent être utilisées pour implémenter des unions marquées. Une utilisation pratique consiste à implémenter Rust
Result<T, E>
, qui est à l'origine implémenté à l'aide d'un purenum
(Rust peut contenir des données supplémentaires dans des variantes d'énumération). Voici un exemple C ++:la source