Objectif des unions en C et C ++

254

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.

legends2k
la source
11
En termes simples, les compilateurs sont autorisés à insérer un remplissage entre les éléments d'une structure. Ainsi, b, g, r,et apeut ne pas être contiguë, et donc ne pas correspondre à la disposition d'un uint32_t. Cela s'ajoute aux problèmes d'endianisme que d'autres ont signalés.
Thomas Matthews
8
C'est exactement pourquoi vous ne devriez pas étiqueter les questions C et C ++. Les réponses sont différentes, mais comme les répondeurs ne disent même pas pour quelle balise ils répondent (savent-ils même?), Vous obtenez des ordures.
Pascal Cuoq
5
@downvoter Merci de ne pas avoir expliqué, je comprends que vous voulez que je comprenne comme par magie votre reproche et que je ne le répète pas à l'avenir: P
legends2k
1
En ce qui concerne l'intention initiale d' union , gardez à l'esprit que la norme C postdate les unions C de plusieurs années. Un rapide coup d'œil à Unix V7 montre quelques conversions de types via les unions.
ninjalj
3
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 "
underscore_d

Réponses:

409

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éfini dé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.

Fourmi
la source
37
+1 pour avoir été élaboré, donner un exemple pratique simple et parler de l'héritage des syndicats!
legends2k
6
Le problème que j'ai avec cette réponse est que la plupart des systèmes d'exploitation que j'ai vus ont des fichiers d'en-tête qui font exactement cela. Par exemple, je l'ai vu dans les anciennes versions (antérieures à 64 bits) de <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.
TED
31
@AndreyT «Il n'a jamais été légal d'utiliser des syndicats pour le type punning jusqu'à très récemment»: 2004 n'est pas «très récent», d'autant plus que ce n'est que C99 qui était initialement maladroitement libellé, semblant rendre le type punning via les syndicats indéfini. En réalité, la punition par type dans les syndicats est légale en C89, légale en C11, et elle était légale en C99 depuis le début, bien qu'il ait fallu attendre 2004 pour que le comité corrige une formulation incorrecte et la publication ultérieure de TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq
6
@ legends2k Le langage de programmation est défini par standard. Le corrigendum technique 3 de la norme C99 autorise explicitement le marquage de type dans sa note de bas de page 82, que je vous invite à lire par vous-même. Ce n'est pas la télévision où des rock stars sont interviewées et expriment leurs opinions sur le changement climatique. L'avis de Stroustrup n'a aucune influence sur ce que dit la norme C.
Pascal Cuoq
6
@ legends2k " Je sais que l'opinion de tout individu n'a pas d'importance et seule la norme le fait " L'opinion des rédacteurs du compilateur importe beaucoup plus que la "spécification" du langage (extrêmement pauvre).
curiousguy
38

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, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Erich Kitzmueller
la source
Je suis tout à fait d'accord, sans entrer dans le chaos des comportements indéfinis, c'est peut-être le meilleur comportement des syndicats auquel je puisse penser; mais ne gaspillera pas d'espace lorsque j'utilise, disons intou char*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?
legends2k
3
légendes: dans certains cas, vous ne pouvez tout simplement pas faire cela. Vous utilisez quelque chose comme VAROBJECT en C dans les mêmes cas lorsque vous utilisez Object en Java.
Erich Kitzmueller
La structure des données des syndicats étiquetés semble être une seule utilisation légitime des syndicats, comme vous l'expliquez.
legends2k
Donnez également un exemple d'utilisation des valeurs.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 六四 事件 法轮功 Une partie d'un exemple de C ++ Primer pourrait vous aider. wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick
34

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_tet 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.

David Rodríguez - dribeas
la source
+1 pour la réponse claire et simple! Je suis d'accord, pour la portabilité, la méthode que vous avez donnée dans le 2e paragraphe est valable; mais puis-je utiliser la façon dont j'ai posé la question, si mon code est lié à une architecture unique (payant le prix de la protabilité), car il enregistre 4 octets pour chaque valeur de pixel et un certain temps gagné en exécutant cette fonction ?
legends2k
Le problème endian ne force pas la norme à le déclarer comme un comportement non défini - reinterpret_cast a exactement les mêmes problèmes endian, mais a un comportement défini par l'implémentation.
JoeG
1
@ legends2k, le problème est que l'optimiseur peut supposer qu'un uint32_t n'est pas modifié en écrivant dans un uint8_t et donc vous obtenez la mauvaise valeur lorsque l'optimisation utilise cette hypothèse ... @Joe, le comportement indéfini apparaît dès que vous accédez au pointeur (je sais, il y a quelques exceptions).
AProgrammer
1
@ legends2k / AProgrammer: Le résultat d'un reinterpret_cast est défini par l'implémentation. L'utilisation du pointeur renvoyé n'entraîne pas de comportement indéfini, uniquement un comportement défini par l'implémentation. En d'autres termes, le comportement doit être cohérent et défini, mais il n'est pas portable.
JoeG
1
@ legends2k: tout optimiseur décent reconnaîtra les opérations au niveau du bit qui sélectionnent un octet entier et génèrent du code pour lire / écrire l'octet, identique à l'union mais bien défini (et portable). par exemple uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | r; }
Ben Voigt
22

L' utilisation la plus courante de unionje 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 accès propre et soigné aux Vector3f vec;membres de a par l'un ou l'autre nom:

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

ou par accès entier dans le tableau

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

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.

bobobobo
la source
3
Ceci est également appelé, type-punningce qui est également mentionné dans la question. L'exemple de la question montre également un exemple similaire.
legends2k
4
Ce n'est pas un type de punition. Dans mon exemple, les types correspondent , donc il n'y a pas de "jeu de mots", c'est simplement un aliasing.
bobobobo
3
Oui, mais quand même, d'un point de vue absolu de la norme linguistique, le membre écrit et lu est différent, ce qui n'est pas défini comme mentionné dans la question.
legends2k
3
J'espère qu'une future norme réglerait ce cas particulier pour qu'il soit autorisé en vertu de la règle de la "sous-séquence initiale commune". Cependant, les tableaux ne participent pas à cette règle dans le libellé actuel.
Ben Voigt
3
@curiousguy: Il n'y a clairement aucune exigence que les membres de la structure soient placés sans remplissage arbitraire. Si le code teste le placement des membres de la structure ou la taille de la structure, le code devrait fonctionner si les accès se font directement via l'union, mais une lecture stricte de la norme indiquerait que la prise de l'adresse d'un membre d'union ou d'une structure produit un pointeur qui ne peut pas être utilisé. en tant que pointeur de son propre type, mais doit d'abord être reconverti en pointeur vers le type englobant ou un type de caractère. Tout compilateur fonctionnant à distance étendra le langage en faisant fonctionner plus de choses que ...
supercat
10

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.

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 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
L'avez-vous utilisé ainsi (comme dans la question) ?? :)
legends2k
C'est un peu pédant, mais je n'accepte pas tout à fait les "variantes de disques". Autrement dit, je suis sûr qu'ils étaient à l'esprit, mais s'ils étaient une priorité, pourquoi ne pas les fournir? "Fournir le bloc de construction, car il pourrait être utile de construire d'autres choses également" semble intuitivement plus probable. Surtout étant donné au moins une autre application qui était probablement à l'esprit - les registres d'E / S mappés en mémoire, où les registres d'entrée et de sortie (lorsqu'ils se chevauchent) sont des entités distinctes avec leurs propres noms, types, etc.
Steve314
@ Stev314 Si c'était l'usage qu'ils avaient en tête, ils auraient pu faire en sorte que ce ne soit pas un comportement indéfini.
@Neil: +1 pour le premier à dire sur l'utilisation réelle sans toucher à un comportement indéfini. Je suppose qu'ils auraient pu faire en sorte que l'implémentation soit définie comme d'autres opérations de punning de type (reinterpret_cast, etc.). Mais comme je l'ai demandé, l'avez-vous utilisé pour le type-punning?
legends2k
@Neil - l'exemple de registre mappé en mémoire n'est pas indéfini, l'endian / etc habituel mis à part et étant donné un indicateur "volatile". L'écriture à une adresse dans ce modèle ne fait pas référence au même registre que la lecture de la même adresse. Par conséquent, il n'y a pas de problème "que lisez-vous" car vous ne lisez pas - quelle que soit la sortie que vous avez écrite à cette adresse, lorsque vous lisez, vous lisez simplement une entrée indépendante. Le seul problème est de vous assurer de lire le côté entrée de l'union et d'écrire le côté sortie. Était courant dans les trucs intégrés - l'est probablement toujours.
Steve314
8

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;

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

    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 des bits.

Totonga
la source
Bien que vos deux exemples soient parfaitement définis dans la norme; mais, hé, utiliser des champs de bits est sûr de tirer du code non portable, n'est-ce pas?
legends2k
Non ça ne l'est pas. Pour autant que je sache, son large soutien.
Totonga
1
Le support du compilateur ne se traduit pas en portable. Le livre C : C (donc C ++) ne donne aucune garantie de l'ordre des champs dans les mots machine, donc si vous les utilisez pour cette dernière raison, votre programme sera non seulement non portable, mais aussi dépendant du compilateur.
legends2k
5

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.

Paul R
la source
2
N'y a-t-il pas un problème endien? Un correctif relativement facile par rapport à "non défini", mais qui vaut la peine d'être pris en compte pour certains projets dans l'affirmative.
Steve314
5

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 + unionconstruction (pile allouée aussi etc) mais il utilise une liste de modèles de types au lieu de enum:)

Matthieu M.
la source
5

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).

pseudo
la source
4

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_castd'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.

JoeG
la source
" un reinterpret_cast d'un type à l'autre, dont le résultat est défini par l'implémentation. " Non, ce n'est pas le cas. Les implémentations n'ont pas à le définir, et la plupart ne le définissent pas. En outre, quel serait le comportement défini par l'implémentation autorisé de transtyper une valeur aléatoire en un pointeur?
curiousguy
4

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.

Cubbi
la source
4

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].

philcolbourn
la source
Un octet qui doit être aligné sur un mot? Ça n'a aucun sens. Un octet n'a aucune exigence d'alignement, par définition.
curiousguy
Oui, j'aurais probablement dû utiliser un meilleur exemple. Merci.
philcolbourn
@curiousguy: Il existe de nombreux cas où l'on peut souhaiter que les tableaux d'octets soient alignés sur les mots. Si l'on a de nombreux tableaux de 1 024 octets par exemple et que l'on souhaite souvent les copier les uns les autres, les faire aligner par mot peut sur de nombreux systèmes doubler la vitesse d'un memcpy()de l'un à l'autre. Certains systèmes peuvent aligner de manière spéculative les char[]allocations qui se produisent en dehors des structures / unions pour cela et pour d'autres raisons. Dans l'exemple actuel, l'hypothèse qui ichevauchera tous les éléments de c[]n'est pas portable, mais c'est parce qu'il n'y a aucune garantie sizeof(int)==4.
supercat
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.

supercat
la source
Appréciez la leçon d'histoire, mais avec la norme définissant telle ou telle chose non définie, ce qui n'était pas le cas à l'ère C révolue où le livre K&R était la seule "norme", il faut être sûr de ne pas l'utiliser à quelque fin que ce soit et entrez dans le pays UB.
legends2k
2
@ legends2k: Lorsque la norme a été écrite, la majorité des implémentations C traitaient les unions de la même manière, et un tel traitement était utile. Cependant, quelques-uns ne l'ont pas fait et les auteurs de la norme répugnaient à qualifier toute implémentation existante de "non conforme". Au lieu de cela, ils ont pensé que si les implémenteurs n'avaient pas besoin de la norme pour leur dire de faire quelque chose (comme en témoigne le fait qu'ils le faisaient déjà ), le laisser non spécifié ou non défini préserverait simplement le statu quo . L'idée qu'elle devrait rendre les choses moins définies qu'elles ne l'étaient avant la rédaction de la norme ...
supercat
2
... semble une innovation beaucoup plus récente. Ce qui est particulièrement triste à propos de tout cela, c'est que si les rédacteurs de compilateurs ciblant les applications haut de gamme devaient trouver comment ajouter des directives d'optimisation utiles au langage que la plupart des compilateurs ont mis en œuvre dans les années 1990, plutôt que de vider les fonctionnalités et les garanties qui avaient été prises en charge par "seulement "90% des implémentations, le résultat serait un langage qui pourrait être plus performant et plus fiable que le C. hyper-moderne
supercat
2

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. Un moyen d'économiser de l'espace lorsqu'il existe différents membres de données dont un seul peut jamais être «actif»

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.

Mr. Boy
la source
2

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 pur enum(Rust peut contenir des données supplémentaires dans des variantes d'énumération). Voici un exemple C ++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
Kotauskas
la source