J'ai appris mais je n'ai pas vraiment de syndicats. Chaque texte C ou C++ que je parcoure les introduit (parfois en passant), mais ils ont tendance à donner très peu d'exemples pratiques de pourquoi ou de l'endroit où les utiliser. Quand les syndicats seraient-ils utiles dans un cas moderne (ou même hérité)? Mon seul doute: programmer des microprocesseurs lorsque vous avez peu d’espace sur lequel travailler ou que vous développez une API (ou quelque chose de similaire) et que vous voulez forcer l’utilisateur final à ne posséder qu’une instance de plusieurs une fois. Ces deux suppositions sont-elles encore proches de la droite?
Les syndicats sont généralement utilisés avec l'entreprise d'un discriminateur: une variable indiquant lequel des champs du syndicat est valide. Par exemple, supposons que vous souhaitiez créer votre propre Variant type:
struct my_variant_t {
int type;
union {
char char_value;
short short_value;
int int_value;
long long_value;
float float_value;
double double_value;
void* ptr_value;
};
};
Ensuite, vous l'utiliseriez comme:
/* construct a new float variant instance */
void init_float(struct my_variant_t* v, float initial_value) {
v->type = VAR_FLOAT;
v->float_value = initial_value;
}
/* Increments the value of the variant by the given int */
void inc_variant_by_int(struct my_variant_t* v, int n) {
switch (v->type) {
case VAR_FLOAT:
v->float_value += n;
break;
case VAR_INT:
v->int_value += n;
break;
...
}
}
C'est en fait un idiome assez courant, spécialement sur les internes de Visual Basic.
Pour un exemple réel, voir SDL_Event union . ( code source actuel ici ). Il y a un champ type
en haut de l'union et le même champ est répété sur chaque structure d'événement SDL_ *. Ensuite, pour gérer l'événement correct, vous devez vérifier la valeur du champ type
.
Les avantages sont simples: il existe un seul type de données pour gérer tous les types d'événements sans utiliser de mémoire inutile.
Je trouve les unions C++ plutôt cool. Il semble que les gens ne pensent généralement qu'au cas d'utilisation où l'on souhaite modifier la valeur d'une instance d'union "en place" (ce qui, semble-t-il, ne sert qu'à économiser de la mémoire ou à effectuer des conversions douteuses).
En fait, les syndicats peuvent avoir un grand pouvoir en tant qu’outil de génie logiciel, même lorsque vous ne modifiez jamais la valeur d'une instance d'union.
Avec les unions, vous pouvez regrouper un certain nombre de classes arbitraires sous une même dénomination, ce qui n'est pas sans similitudes avec le cas d'une classe de base et de ses classes dérivées. Ce qui change, cependant, est ce que vous pouvez et ne pouvez pas faire avec une instance d'union donnée:
struct Batman;
struct BaseballBat;
union Bat
{
Batman brucewayne;
BaseballBat club;
};
ReturnType1 f(void)
{
BaseballBat bb = {/* */};
Bat b;
b.club = bb;
// do something with b.club
}
ReturnType2 g(Bat& b)
{
// do something with b, but how do we know what's inside?
}
Bat returnsBat(void);
ReturnType3 h(void)
{
Bat b = returnsBat();
// do something with b, but how do we know what's inside?
}
Il apparaît que le programmeur doit être certain du type de contenu d'une instance d'union donnée lorsqu'il veut l'utiliser. C'est le cas dans la fonction f
ci-dessus. Cependant, si une fonction recevait une instance d'union sous la forme d'un argument passé, comme c'est le cas avec g
ci-dessus, elle ne saurait pas quoi en faire. La même chose s'applique aux fonctions renvoyant une instance d'union, voir h
: comment l'appelant sait-il ce qu'il y a à l'intérieur?
Si une instance d'union n'est jamais transmise en tant qu'argument ou valeur de retour, elle aura forcément une vie très monotone, avec des pics d'excitation lorsque le programmeur choisit de modifier son contenu:
Batman bm = {/* */};
Baseball bb = {/* */};
Bat b;
b.brucewayne = bm;
// stuff
b.club = bb;
Et c’est l’utilisation la plus (in) populaire des syndicats. Un autre cas d'utilisation survient lorsqu'une instance d'union vient avec quelque chose qui vous indique son type.
object
, de Class
"Supposons qu'un programmeur ait choisi de toujours coupler une instance d'union avec un descripteur de type (je laisserai au lecteur le soin d'imaginer une implémentation pour un tel objet). Cela va à l'encontre de l'objectif de l'union elle-même si le programmeur souhaite économiser de la mémoire et que la taille du descripteur de type n'est pas négligeable par rapport à celle de l'union. Mais supposons qu'il soit crucial que l'instance d'union puisse être transmise sous forme d'argument ou de valeur de retour, l'appelé ou l'appelant ne sachant pas ce qu'il contient.
Ensuite, le programmeur doit écrire un état de flux de contrôle switch
pour indiquer à Bruce Wayne un bâton en bois ou un objet équivalent. Ce n'est pas si mal quand il n'y a que deux types de contenu dans le syndicat mais que, de toute évidence, le syndicat ne s'adapte plus.
En tant qu'auteurs de une recommandation pour la norme ISO C++ l'a remise en 2008,
De nombreux domaines problématiques importants nécessitent soit un grand nombre d'objets, soit une mémoire limitée Ressources. Dans ces situations, la conservation de l'espace est très importante et une union est souvent un moyen idéal de le faire. En fait, un cas d'utilisation courant est le cas où un syndicat ne change jamais de membre actif au cours de sa vie. Il peut être construit, copié et détruit comme s'il s'agissait d'une structure ne contenant qu'un seul membre. Une application typique de cela serait de créer une collection hétérogène de types non liés qui ne sont pas alloués dynamiquement (peut-être sont-ils construits sur place dans une carte ou des membres d'un tableau).
Et maintenant, un exemple avec un diagramme de classes UML:
La situation en clair: un objet de classe A pouvez avoir des objets de n'importe quelle classe parmi B1, ..., Bn et au plus un de chaque type, avec n être un assez grand nombre, disons au moins 10.
Nous ne voulons pas ajouter de champs (membres de données) à A comme suit:
private:
B1 b1;
.
.
.
Bn bn;
parce que n pourrait varier (nous pourrions vouloir ajouter des classes Bx au mélange), et parce que cela causerait un désordre avec les constructeurs et parce que les objets A prendraient beaucoup d'espace.
Nous pourrions utiliser un conteneur loufoque de pointeurs void*
vers des objets Bx
avec des transtypages pour les récupérer, mais c'est fugly et donc de style C ... mais plus important encore, cela nous laisserait le temps de gérer de nombreux objets alloués dynamiquement.
Au lieu de cela, voici ce qui peut être fait:
union Bee
{
B1 b1;
.
.
.
Bn bn;
};
enum BeesTypes { TYPE_B1, ..., TYPE_BN };
class A
{
private:
std::unordered_map<int, Bee> data; // C++11, otherwise use std::map
public:
Bee get(int); // the implementation is obvious: get from the unordered map
};
Ensuite, pour obtenir le contenu d'une instance d'union à partir de data
, vous utilisez a.get(TYPE_B2).b2
et les likes, où a
est une instance de classe A
.
C’est d’autant plus puissant que les unions sont illimitées en C++ 11. Voir le document lié à ci-dessus ou cet article pour plus de détails.
Un exemple est dans le domaine imbriqué, où chaque bit d'un registre peut signifier quelque chose de différent. Par exemple, l'union d'un entier de 8 bits et d'une structure avec 8 champs de bits séparés de 1 bit permet de modifier un bit ou l'intégralité de l'octet.
Herb Sutter a écrit dans GOTW il y a environ six ans, avec italiques a ajouté:
"Mais ne pensez pas que les unions ne sont qu'une réserve des années précédentes. Elles sont peut-être plus utiles pour économiser de l'espace en permettant le chevauchement des données, et cela est toujours souhaitable en C++ et dans le contexte actuel. Par exemple, certaines des implémentations de librairies standard les plus avancées du monde C++ utilisent maintenant cette technique pour implémenter la "petite optimisation de chaîne", une excellente alternative d'optimisation qui réutilise le stockage. dans un objet chaîne lui-même: pour les grandes chaînes, l'espace à l'intérieur de l'objet chaîne stocke le pointeur habituel sur le tampon alloué dynamiquement et des informations de gestion comme la taille du tampon, tandis que pour les petites chaînes, le même espace est réutilisé pour stocker directement le contenu de la chaîne. et évitez complètement toute allocation de mémoire dynamique. Pour en savoir plus sur l'optimisation des petites chaînes (et d'autres optimisations et pessimisations de chaînes très détaillées), voir .... "
Et pour un exemple moins utile, voyez la question longue mais peu concluante gcc, aliasing strict et conversion via une union .
Eh bien, un exemple d'utilisation auquel je peux penser est le suivant:
typedef union
{
struct
{
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
};
uint32_t x;
} some32bittype;
Vous pouvez ensuite accéder aux parties séparées de 8 bits de ce bloc de données de 32 bits; Cependant, préparez-vous à être potentiellement mordu par l’endianité.
Ce n'est qu'un exemple hypothétique, mais vous pouvez utiliser une union chaque fois que vous souhaitez scinder des données d'un champ en composants.
Cela dit, il existe aussi une méthode qui est endian-safe:
uint32_t x;
uint8_t a = (x & 0xFF000000) >> 24;
Par exemple, puisque cette opération binaire sera convertie par le compilateur en endianité correcte.
Les syndicats sont utiles pour traiter des données au niveau octet (niveau bas).
Une de mes utilisations récentes portait sur la modélisation des adresses IP, qui se présente comme suit:
// Composite structure for IP address storage
union
{
// IPv4 @ 32-bit identifier
// Padded 12-bytes for IPv6 compatibility
union
{
struct
{
unsigned char _reserved[12];
unsigned char _IpBytes[4];
} _Raw;
struct
{
unsigned char _reserved[12];
unsigned char _o1;
unsigned char _o2;
unsigned char _o3;
unsigned char _o4;
} _Octet;
} _IPv4;
// IPv6 @ 128-bit identifier
// Next generation internet addressing
union
{
struct
{
unsigned char _IpBytes[16];
} _Raw;
struct
{
unsigned short _w1;
unsigned short _w2;
unsigned short _w3;
unsigned short _w4;
unsigned short _w5;
unsigned short _w6;
unsigned short _w7;
unsigned short _w8;
} _Word;
} _IPv6;
} _IP;
Quelques utilisations pour les syndicats:
union { unsigned char byte_v[16]; long double ld_v; }
Avec cette déclaration, il est simple d’afficher les octets hexadécimaux d’un
long double
, de changer le signe de l’exposant, de déterminer s’il s’agit d’une valeur dénormale ou d’implémenter une double arithmétique longue pour une CPU qui ne la supporte pas, etc.
Économie d'espace de stockage lorsque les champs dépendent de certaines valeurs:
class person {
string name;
char gender; // M = male, F = female, O = other
union {
date castrated; // for males
int pregnancies; // for females
} gender_specific_data;
}
Grep les fichiers d'inclusion à utiliser avec votre compilateur. Vous trouverez des dizaines à des centaines d'utilisations de union
:
[wally@zenetfedora ~]$ cd /usr/include
[wally@zenetfedora include]$ grep -w union *
a.out.h: union
argp.h: parsing options, getopt is called with the union of all the argp
bfd.h: union
bfd.h: union
bfd.h:union internal_auxent;
bfd.h: (bfd *, struct bfd_symbol *, int, union internal_auxent *);
bfd.h: union {
bfd.h: /* The value of the symbol. This really should be a union of a
bfd.h: union
bfd.h: union
bfdlink.h: /* A union of information depending upon the type. */
bfdlink.h: union
bfdlink.h: this field. This field is present in all of the union element
bfdlink.h: the union; this structure is a major space user in the
bfdlink.h: union
bfdlink.h: union
curses.h: union
db_cxx.h:// 4201: nameless struct/union
elf.h: union
elf.h: union
elf.h: union
elf.h: union
elf.h:typedef union
_G_config.h:typedef union
gcrypt.h: union
gcrypt.h: union
gcrypt.h: union
gmp-i386.h: union {
ieee754.h:union ieee754_float
ieee754.h:union ieee754_double
ieee754.h:union ieee854_long_double
ifaddrs.h: union
jpeglib.h: union {
ldap.h: union mod_vals_u {
ncurses.h: union
newt.h: union {
obstack.h: union
pi-file.h: union {
resolv.h: union {
signal.h:extern int sigqueue (__pid_t __pid, int __sig, __const union sigval __val)
stdlib.h:/* Lots of hair to allow traditional BSD use of `union wait'
stdlib.h: (__extension__ (((union { __typeof(status) __in; int __i; }) \
stdlib.h:/* This is the type of the argument to `wait'. The funky union
stdlib.h: causes redeclarations with either `int *' or `union wait *' to be
stdlib.h:typedef union
stdlib.h: union wait *__uptr;
stdlib.h: } __WAIT_STATUS __attribute__ ((__transparent_union__));
thread_db.h: union
thread_db.h: union
tiffio.h: union {
wchar.h: union
xf86drm.h:typedef union _drmVBlank {
Un exemple lorsque j'ai utilisé un syndicat:
class Vector
{
union
{
double _coord[3];
struct
{
double _x;
double _y;
double _z;
};
};
...
}
cela me permet d'accéder à mes données sous forme de tableau ou d'éléments.
J'ai utilisé un syndicat pour que les termes différents indiquent la même valeur. En traitement d'image, que je travaille sur des colonnes ou sur une largeur ou une taille dans la direction X, cela peut devenir confus. Pour alléguer ce problème, j'utilise un syndicat afin de savoir quelles descriptions vont de pair.
union { // dimension from left to right // union for the left to right dimension
uint32_t m_width;
uint32_t m_sizeX;
uint32_t m_columns;
};
union { // dimension from top to bottom // union for the top to bottom dimension
uint32_t m_height;
uint32_t m_sizeY;
uint32_t m_rows;
};
Les syndicats fournissent un polymorphisme en C.
Une utilisation brillante de l'union est l'alignement de la mémoire, que j'ai trouvé dans le code source PCL (Point Cloud Library). La structure de données unique de l'API peut cibler deux architectures: une CPU avec prise en charge de SSE ainsi que la CPU sans prise en charge de SSE. Par exemple: la structure de données pour PointXYZ est
typedef union
{
float data[4];
struct
{
float x;
float y;
float z;
};
} PointXYZ;
Les 3 flotteurs sont rembourrés avec un flotteur supplémentaire pour un alignement SSE. Donc pour
PointXYZ point;
L'utilisateur peut accéder à point.data [0] ou à point.x (en fonction du support SSE) pour accéder à, par exemple, la coordonnée x . Des détails d'utilisation plus similaires sont disponibles sur le lien suivant: PCL documentation PointT types
Le mot clé union
, bien qu'utilisé en C++ 031, est surtout un vestige des jours C. Le problème le plus criant est que cela ne fonctionne qu'avec POD1.
L'idée de l'union, cependant, est toujours présente, et en effet les bibliothèques Boost proposent une classe semblable à celle d'un syndicat:
boost::variant<std::string, Foo, Bar>
Ce qui présente le plus d'avantages de la variable union
(sinon la totalité) et ajoute:
En pratique, il a été démontré que cela équivalait à une combinaison de union
+ enum
, et a été évalué comme étant aussi rapide (alors que boost::any
est plutôt du domaine de dynamic_cast
, puisqu'il utilise RTTI).
1Les unions ont été mises à niveau en C++ 11 ( unions non restreintes ) et peuvent désormais contenir des objets avec des destructeurs, bien que l'utilisateur doive appeler le destructeur manuellement (sur le membre de l'union actuellement actif). Il est encore beaucoup plus facile d'utiliser des variantes.
Extrait du Article Wikipedia sur les syndicats :
La principale utilité d’une union est pour conserver de l'espace, car il fournit un façon de laisser de nombreux types différents être stocké dans le même espace. Les syndicats aussi fournit un polymorphisme brut. Toutefois, il n'y a pas de vérification des types, donc ça est au programmeur d’en être sûr que les champs appropriés sont accessibles dans différents contextes. Le champ concerné d'une variable d'union est typiquement déterminé par l'état de l'autre variables, éventuellement dans un struct.
Un langage de programmation C courant utilise syndicats pour effectuer ce que C++ appelle un réinterpréter_cast, en attribuant à un champ d'une union et lecture de un autre, comme dans le code qui dépend de la représentation brute de les valeurs.
Disons que vous avez n différents types de configurations (juste un ensemble de variables définissant des paramètres). En utilisant une énumération des types de configuration, vous pouvez définir une structure qui possède l'ID du type de configuration, avec une union de tous les différents types de configurations.
De cette façon, chaque fois que vous passez la configuration, vous pouvez utiliser l'ID pour déterminer comment interpréter les données de configuration. Toutefois, si les configurations étaient énormes, vous ne seriez pas obligé d'avoir des structures parallèles pour chaque type de perte d'espace.
Un accent récent sur l’importance déjà élevée de la union s a été donné par la Strict Aliasing Rule introduite dans la version récente du standard C.
Vous pouvez utiliser les unions pour taper-punning sans enfreindre le standard C.
Ce programme a un comportement non spécifié (car j’ai supposé que float
et unsigned int
ont la même longueur) mais pas le comportement undefined (voir ici ).
#include <stdio.h>
union float_uint
{
float f;
unsigned int ui;
};
int main()
{
float v = 241;
union float_uint fui = {.f = v};
//May trigger UNSPECIFIED BEHAVIOR but not UNDEFINED BEHAVIOR
printf("Your IEEE 754 float sir: %08x\n", fui.ui);
//This is UNDEFINED BEHAVIOR as it violates the Strict Aliasing Rule
unsigned int* pp = (unsigned int*) &v;
printf("Your IEEE 754 float, again, sir: %08x\n", *pp);
return 0;
}
Je voudrais ajouter un bon exemple pratique d'utilisation d'union en utilisant un calculateur/interprète de formules ou en utilisant une sorte de calcul (par exemple, vous voulez utiliser modifiable pendant l'exécution des parties de vos formules informatiques - résoudre équation numérique - juste par exemple) . Donc, vous voudrez peut-être définir des nombres/constantes de types différents (nombres entiers, nombres à virgule flottante, voire complexes) comme ceci:
struct Number{
enum NumType{int32, float, double, complex}; NumType num_t;
union{int ival; float fval; double dval; ComplexNumber cmplx_val}
}
Donc, vous économisez de la mémoire et, ce qui est plus important, vous évitez toute allocation dynamique pour une quantité probablement extrême (si vous utilisez beaucoup de nombres définis au moment de l'exécution) de petits objets (par rapport aux implémentations via l'héritage/polymorphisme de classe). Mais plus intéressant encore, vous pouvez toujours utiliser la puissance du polymorphisme C++ (si vous êtes fan de la double répartition, par exemple;) avec ce type de structure. Il suffit d’ajouter un pointeur «fictif» à la classe parente de tous les types de nombres en tant que champ de cette structure, pointant vers cette instance au lieu de/en plus du type brut, ou d’utiliser de bons anciens pointeurs de fonction C.
struct NumberBase
{
virtual Add(NumberBase n);
...
}
struct NumberInt: Number
{
//implement methods assuming Number's union contains int
NumberBase Add(NumberBase n);
...
}
struct NumberDouble: Number
{
//implement methods assuming Number's union contains double
NumberBase Add(NumberBase n);
...
}
//e.t.c. for all number types/or use templates
struct Number: NumberBase{
union{int ival; float fval; double dval; ComplexNumber cmplx_val;}
NumberBase* num_t;
Set(int a)
{
ival=a;
//still kind of hack, hope it works because derived classes of Number dont add any fields
num_t = static_cast<NumberInt>(this);
}
}
vous pouvez donc utiliser le polymorphisme au lieu des vérifications de type avec switch (type) - avec une implémentation économe en mémoire (pas d'allocation dynamique de petits objets) - si vous en avez besoin, bien sûr.
De http://cplus.about.com/od/learningc/ss/lowlevel_9.htm :
Les utilisations de l'union sont rares. Sur la plupart des ordinateurs, la taille d’un pointeur et d’un int sont généralement les mêmes, c’est parce que les deux habituellement dans un registre dans la CPU. Donc, si vous voulez faire un rapide et sale casting d'un pointeur sur un int ou l'inverse, déclarez un syndicat.
union intptr { int i; int * p; }; union intptr x; x.i = 1000; /* puts 90 at location 1000 */ *(x.p)=90;
Une autre utilisation d'une union est dans un protocole de commande ou de message où des messages de taille différente sont envoyés et reçus. Chaque type de message sera contenir des informations différentes mais chacune aura une partie fixe (probablement une structure ) et un bit de partie variable. Voici comment vous pourriez l’appliquer.
struct head { int id; int response; int size; }; struct msgstring50 { struct head fixed; char message[50]; } struct
struct msgstring80 {struct head fixe; message de caractère [80]; }
struct msgint10 {struct head fixe; int message [10]; } struct msgack {struct head fixe; int ok } union messagetype {
struct msgstring50 m50; struct msgstring80 m80; struct msgint10 i10; struct msgack ack; }En pratique, bien que les syndicats aient la même taille, il est logique de envoyer seulement les données significatives et pas de l'espace gaspillé. Un msgack est juste 16 octets en taille alors qu'un msgstring80 est 92 octets. Alors, quand un La variable messagetype est initialisée, son champ de taille est défini selon quel type il est. Ceci peut alors être utilisé par d’autres fonctions pour transférer le nombre correct d'octets.
Aux premiers jours de C (par exemple, comme documenté en 1974), toutes les structures partageaient un espace de noms commun pour leurs membres. Chaque nom de membre était associé à un type et à un offset; Si "wd_woozle" était un "int" à l'offset 12, alors étant donné un pointeur p
de tout type de structure, p->wd_woozle
serait équivalent à *(int*)(((char*)p)+12)
. La langue demandait que tous les membres de tous les types de structures aient des noms uniques sauf qu’elle autorisait explicitement la réutilisation des noms de membres dans les cas où chaque structure où ils étaient utilisés les traitait comme une séquence initiale commune.
Le fait que les types de structure puissent être utilisés de manière prompte permet de faire en sorte que les structures se comportent comme si elles contenaient des champs se chevauchant. Par exemple, les définitions données:
struct float1 { float f0;};
struct byte4 { char b0,b1,b2,b3; }; /* Unsigned didn't exist yet */
le code pourrait déclarer une structure de type "float1" puis utiliser "membres" b0 ... b3 pour accéder aux octets individuels qui y sont contenus. Lorsque la langue a été modifiée de manière à ce que chaque structure reçoive un espace de noms distinct pour ses membres, le code reposant sur la possibilité d'accéder à des éléments de plusieurs manières se briserait. Les valeurs de séparation des espaces de noms pour différents types de structure étaient suffisantes pour exiger la modification de ce code, mais la valeur de telles techniques était suffisante pour justifier une extension du langage pour continuer à le prendre en charge.
Le code qui avait été écrit pour exploiter la possibilité d’accéder au stockage dans un struct float1
comme si c’était un struct byte4
pouvait fonctionner dans la nouvelle langue en ajoutant une déclaration: union f1b4 { struct float1 ff; struct byte4 bb; };
, en déclarant des objets de type union f1b4;
au lieu de struct float1
, et en remplaçant les accès à f0
, b0
, b1
, etc. avec ff.f0
, bb.b0
, bb.b1
, etc. Même s'il existe de meilleures façons de prendre en charge un tel code, l'approche union
était à tout le moins réalisable, du moins avec les interprétations des règles d'alias en C89 .
Les syndicats offrent un moyen de manipuler différents types de données dans une seule zone de stockage sans incorporer aucune information indépendante de la machine dans le programme Ils sont analogues aux enregistrements variantes de Pascal.
À titre d'exemple, par exemple, dans un gestionnaire de table de symboles du compilateur, supposons qu'une constante .__ puisse être un int, un float ou un pointeur de caractère. La valeur d'une constante particulière Doit être stockée dans une variable du type approprié. Cependant, il est plus pratique pour la gestion des tables si la valeur occupe la même quantité de stockage et est stockée au même endroit quel que soit son type. C'est le but d'une union - une seule variable qui peut légitimement contenir l'un de plusieurs types. La syntaxe est basée sur les structures:
union u_tag {
int ival;
float fval;
char *sval;
} u;
La variable u sera assez grande pour contenir le plus grand des trois types; la taille spécifique dépend de l'implémentation. Chacun de ces types peut être affecté à u puis utilisé dans les expressions , Tant que l'utilisation est cohérente