web-dev-qa-db-fra.com

But des unions en C et C ++

J'ai utilisé des syndicats plus tôt confortablement; aujourd'hui, j'ai été alarmé quand j'ai lu ce post et suis venu pour 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 lire un membre du syndicat autre que celui qui a été écrit récemment conduit à 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 s'il vous plaît expliquer de manière détaillée?

Mise à jour:

Je voulais clarifier quelques points avec le recul.

  • La réponse à la question n'est pas la même pour C et C++; mes jeunes ignorants l'ont étiquetée à la fois en C et en C++.
  • Après avoir parcouru le standard C++ 11, je ne pouvais pas affirmer avec certitude qu'il appelait à accéder/inspecter un membre non actif de l'union n'est pas défini/non spécifié/défini par la mise en œuvre. Tout ce que je pouvais trouver était §9.5/1:

    Si une union de présentation standard contient plusieurs structures d’agencement standard qui partagent une séquence initiale commune, et si un objet de ce type d’union de présentation standard contient l’une des structures de présentation standard, il est autorisé à inspecter la séquence initiale commune de des membres de structure standard-layout. §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 que ni l'un ni l'autre n'est un champ de bits, ni les deux, des champs de bits de même largeur pour une séquence d'un ou plusieurs membres.

  • En C ( C99 TC3 - DR 28 ), il est légal de le faire ( merci à Pascal Cuoq pour l'avoir signalé). Cependant, si vous tentez de faire cela peut toujours entraîner un comportement indéfini, si la valeur lue s'avère invalide (appelée "représentation de trappe") 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 l’objectif d’une union - une seule variable pouvant légitimement être retenue parmi plusieurs types. [...] tant que l'usage est cohérent: le type récupéré doit être le type le plus récemment stocké. Il incombe au programmeur de savoir quel type est actuellement stocké dans une union; les résultats dépendent de la mise en œuvre si quelque chose est stocké sous un type et extrait sous un autre.

  • Extrait du TC++ PL de Stroustrup (Mine d'emphase)

    L'utilisation d'unions peut être essentielle pour la compatibilité des données [...] parfois mal utilisé 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 syndicats ET non sur ce que la norme permet P.g. L'utilisation de l'héritage pour la réutilisation de code est bien entendu autorisée par le standard C++, mais 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 continue à rester celle acceptée.

225
legends2k

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

Le but de l'union est de sauvegarder de la mémoire en utilisant la même région de mémoire pour stocker différents objets à des moments différents. il.

C'est comme une chambre dans un hôtel. Différentes personnes y vivent pour des périodes ne se chevauchant 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 dans les chambres (en s'assurant que différentes personnes ne sont pas affectées à une seule et même chambre à la fois), un hôtel relativement petit peut héberger un nombre relativement important de personnes. sont pour.

C'est exactement ce que fait l'union. Si vous savez que plusieurs objets de votre programme contiennent des valeurs dont la durée de vie ne se chevauche pas, vous pouvez alors "fusionner" ces objets 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 instant du programme. Seul le membre "actif" peut être lu. En écrivant dans un autre membre, vous basculez le statut "actif" sur cet autre membre.

Pour une raison quelconque, cet objectif initial du syndicat a été "remplacé" par quelque chose de complètement différent: écrire un membre d'un syndicat puis l'inspecter par le biais d'un autre membre. Ce type de réinterprétation de la mémoire (aka "type punning") est pas une utilisation valide des syndicats. Cela conduit généralement à un comportement indéfini est décrit comme produisant un comportement défini par la mise en oeuvre dans C89/90.

EDIT: L'utilisation d'unions aux fins de typage (c'est-à-dire écrire un membre puis en lire un autre) a été définie plus en détail dans l'un des rectificatifs techniques. conforme à la norme C99 (voir DR # 257 et DR # 28 ). Cependant, gardez à l'esprit que, formellement, cela ne vous empêche pas de rencontrer un comportement indéfini en tentant de lire une représentation d'interruption.

356
AnT

Vous pouvez utiliser des unions pour créer des structures telles que celle-ci, qui contient un champ nous indiquant 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;
36
Erich Kitzmueller

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 et d’endianité de la mémoire. Le code dans une machine big 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 finalité (et les mêmes contraintes d'alignement mémoire ...) en limitant l'utilisation.

Si vous utilisez C++ (vous utilisez deux balises) et que vous tenez à la portabilité, vous pouvez simplement utiliser la structure et fournir un outil de configuration prenant le uint32_t et définit les champs de manière appropriée via des opérations de masque de bits. La même chose peut être faite en C avec une fonction.

Edit: Je m'attendais à ce que AProgrammer écrive une réponse pour voter et ferme celle-ci. Comme certains commentaires l'ont souligné, la finalité 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 gérés différemment. Les règles de repliement strictes auxquelles AProgrammer se réfère implicitement constituent un point important ici. Le compilateur est autorisé à émettre des hypothèses sur la modification (ou le manque de modification) des variables. Dans le cas de l'union, le compilateur pourrait réorganiser les instructions et déplacer la lecture de chaque composant de couleur sur l'écriture dans la variable de couleur.

34

L'utilisation la plus courante de union que je rencontre régulièrement est 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é des membres de Vector3f vec; Par ou par nom:

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

ou par un accès entier au 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, notamment lorsque l'axe est choisi par programme, la chose la plus facile à faire est d'accéder à l'axe par un indice numérique: 0 pour x, 1 pour y et 2 pour z.

17
bobobobo

Comme vous le dites, il s’agit d’un comportement strictement indéfini, même s’il "fonctionnera" sur de nombreuses plateformes. La vraie 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 qu'en C++, les unions ne sont pas d'une grande utilité car elles ne peuvent contenir que des types POD - en réalité ceux sans constructeurs ni destructeurs.

9
anon

En C, c’était un bon moyen 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 mémoire réduite, cette structure utilise moins de mémoire qu'une structure qui a tout le membre.

À propos 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;

accéder aux valeurs de bits.

7
Totonga

En C++, variante Boost implémente une version sécurisée de l'union, conçue pour éviter autant que possible les comportements indéfinis.

Ses performances sont identiques à celles du enum + union _ construction (pile allouée aussi etc.) mais il utilise une liste de modèles de types à la place du enum :)

5
Matthieu M.

Bien que ce comportement soit strictement indéfini, il fonctionnera pratiquement avec n’importe quel compilateur. C'est un paradigme tellement répandu que tout compilateur qui se respecte doit faire "la bonne chose" dans des cas comme celui-ci. C'est certainement préférable au typage, qui peut générer du code cassé avec certains compilateurs.

5
Paul R

Le comportement peut être indéfini, mais cela signifie simplement qu'il n'y a pas de "standard". Tous les compilateurs décents offrent # pragmas pour contrôler l’emballage 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.

En outre, les unions ne sont pas seulement pour économiser de l'espace. Ils peuvent aider les compilateurs modernes avec le typage. Si vous reinterpret_cast<> tout ce que le compilateur ne peut pas présumer de ce que vous faites. Vous devrez peut-être jeter tout ce qu'il sait sur 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).

5
Nick

D'autres ont mentionné les différences d'architecture (petit - gros endian).

Je lis le problème qui dit que puisque la mémoire des variables est partagée, alors en écrivant dans l’une, les autres changent et, en fonction de leur type, la valeur peut ne pas avoir de sens.

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

Ecrire dans x.i n'aurait aucun sens si vous lisiez ensuite dans x.f - à moins que ce ne soit ce que vous vouliez pour regarder les composantes signe, exposant ou mantisse du flottant.

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

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

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

4
philcolbourn

Pour un autre exemple d'utilisation réelle des unions, le framework CORBA sérialise les objets en utilisant l'approche des unions étiquetées. Toutes les classes définies par l'utilisateur sont membres d'une (énorme) union, et un identifiant entier indique au démonstrateur comment interpréter l'union.

4
Cubbi

Techniquement, cela n’est pas défini, mais en réalité la plupart (tous?) Des compilateurs le traitent exactement comme si vous utilisiez un reinterpret_cast d'un type à l'autre, dont le résultat est défini par l'implémentation. Je ne voudrais pas perdre le sommeil sur votre code actuel.

4
JoeG

Dans le langage C documenté en 1974, tous les membres de la structure partageaient un espace de noms commun, et le sens de "ptr-> membre" était défini comme ajoutant le déplacement du membre à "ptr" et accédant au l'adresse résultante en utilisant le type de membre. Cette conception permettait d'utiliser le même ptr avec des noms de membre 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 voyaient attribuer 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’unions au langage a permis d’obtenir la même sémantique que dans les versions précédentes du langage (bien que l’impossibilité d’exporter des noms dans un contexte englobant ait peut-être encore nécessité d’utiliser une fonction de recherche/remplacement pour remplacer foo-> member dans foo-> type1.member). L’important n’était pas tant que les personnes qui ajoutaient les syndicats aient à l’esprit une utilisation particulière de la cible, mais plutôt qu’elles fournissent un moyen par lequel les programmeurs qui s’étaient appuyés sur la sémantique précédente, quel que soit le but, devrait toujours pouvoir atteindre la même sémantique même s’ils devaient utiliser une syntaxe différente pour le faire.

3
supercat

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 de gagner de la place quand il y a différents membres de données dont un seul peut être 'actif'

1 C’est vraiment plus un piratage de style C pour raccourcir le code d’écriture en sachant que l’architecture de mémoire du système cible fonctionne. Comme déjà dit, vous pouvez normalement vous en sortir si vous ne ciblez pas beaucoup de plateformes différentes. Je pense que certains compilateurs pourraient aussi vous laisser utiliser des directives d’emballage (je sais qu’ils le font sur des structures)?

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

2
Mr. Boy

Comme d'autres l'ont mentionné, les unions combinées avec des énumérations et encapsulées dans des structures peuvent être utilisées pour implémenter des unions étiquetées. Une utilisation pratique consiste à implémenter le Result<T, E>, qui est implémenté à l’origine en utilisant un enum pur (Rust peut contenir des données supplémentaires dans les 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;
}
1