Les syndicats ANSI C – sont-ils vraiment utiles?

Hier, en réponse à une question, j’ai appris qu’il était non portable et peu sûr d’écrire dans un membre du syndicat et de lire la valeur d’un autre membre d’un type différent, en supposant l’alignement sous-jacent des membres. Ainsi, après quelques recherches, j’ai trouvé une source écrite qui répète cette affirmation et spécifie un exemple populaire: utiliser l’union int et float pour trouver la représentation binary d’un float.

Alors, comprenant que cette hypothèse n’est pas sûre, je me demande – sauf pour économiser de la mémoire (duh …) quel est l’utilisation réelle des syndicats?

Remarque: c’est-à-dire en vertu de la norme C. Il est clair que, pour une implémentation spécifique, les règles sont connues à l’avance et peuvent être exploitées.

EDIT: le mot “unsafe”, en raison de l’association de ces dernières années, est probablement un mauvais choix de libellé, mais je pense que l’intention est claire.

EDIT 2: Comme ce point se répète dans les réponses – économiser de la mémoire est un argument valide. Je voulais savoir s’il y avait autre chose.

Oui.

Cela fournit un moyen de créer des conteneurs génériques. Cependant, pour obtenir un comportement polymorphe, vous devez implémenter vous-même une vtable ou un type …

Il existe cependant une de ces fonctionnalités que vous n’utilisez que lorsque vous en avez besoin et que vous en avez besoin assez rarement.

Même si les union n’offrent pas beaucoup d’utilité immédiate (en dehors de l’utilisation réduite de la mémoire), l’utilisation d’un union rapport au dumping de tous ses membres dans une struct réside dans le fait qu’elle clarifie la sémantique souhaitée: une seule valeur (ou un ensemble de valeurs). valeurs si c’est une union de struct s) est valide à tout moment. Cela se documente mieux.

L’exclusivité mutuelle des membres serait moins évidente si vous imposiez à tous les membres du union devenir des membres distincts d’une struct . De plus, vous auriez toujours le même problème de comportement mal défini si vous lisiez un membre sur lequel vous n’aviez pas déjà écrit, mais vous devez maintenant également tenir compte de la sémantique de l’application (at-il initialisé tous les membres inutilisés à 0? ça les laisse comme des ordures?), alors dans ce sens, pourquoi ne pas utiliser un syndicat?

Oui, les syndicats peuvent être non portables et dangereux, mais ont des utilités. Par exemple, cela peut accélérer les choses en éliminant la nécessité de convertir un uint32 en caractère char [4]. Cela peut s’avérer utile si vous essayez de router par adresse IP dans SW, mais votre processeur doit être en ordre de réseau. Pensez aux syndicats comme une alternative au moulage, avec moins d’instructions à la machine. Casting a des inconvénients similaires.

La question contient une contrainte qui pourrait empêcher une réponse valide …

Vous posez des questions sur l’utilisation réelle dans le cadre de la norme, mais “utilisation réelle” peut permettre à un programmeur averti d’exploiter le comportement défini par la mise en oeuvre d’une manière que le comité de normalisation ne souhaite ni anticiper ni énumérer. Et je ne veux pas dire que le comité de normalisation avait un comportement particulier à l’esprit, mais qu’il souhaitait explicitement laisser la possibilité d’être exploitée de manière utile.

En d’autres termes: les syndicats n’ont pas besoin d’être utiles pour qu’un comportement défini par défaut soit utile en général, ils peuvent simplement y permettre d’exploiter les bizarreries de leur machine cible sans recourir à l’assemblage.

Il pourrait y avoir un million de façons utiles de les utiliser sur les différentes machines disponibles selon des méthodes définies par la mise en œuvre, et aucune manière utile de les utiliser de manière ssortingctement portable, mais ces millions d’utilisations définies par la mise en œuvre sont une raison suffisante pour normaliser leur existence.

J’espère que cela à du sens.

Même en escomptant une mise en œuvre spécifique où l’alignement et le compactage sont connus, les unions peuvent toujours être utiles.

Ils vous permettent de stocker l’une des nombreuses valeurs dans un seul bloc de mémoire, à savoir:

 typedef struct { int type; union { type1 one; type2 two; } } unioned_type; 

Et oui, il n’est pas portable d’espérer pouvoir stocker vos données dans one fichier et les lire à partir de two . Mais si vous utilisez simplement le type pour spécifier la variable sous-jacente, vous pouvez facilement y accéder sans avoir à transtyper.

En d’autres termes:

 unioned_type ut; ut.type = 1; ut.one = myOne; // Don't use ut.two here unless you know the underlying details. 

c’est bien que vous utilisiez type pour décider qu’une variable type1 est stockée.

Voici une utilisation portable légitime des syndicats:

 struct arg { enum type t; union { intmax_t i; uintmax_t u; long double f; void *p; void (*fp)(void); } v; }; 

Couplé aux informations de type dans t , struct arg peut contenir de manière portable toute valeur numérique ou de pointeur. La structure entière aura probablement une taille de 16 à 32 octets, comparée à 40 à 80 octets si aucune union n’a été utilisée. La différence serait encore plus extrême si je voulais conserver chaque type numérique original possible séparément (caractère car signé, court, int, long, long long, caractère non signé, court non signé, …) plutôt que de les convertir en caractères plus grands. / unsigned / type à virgule flottante avant de les stocker.

De plus, bien qu’il ne soit pas “portable” d’assumer quoi que ce soit à propos de la représentation de types autres que unsigned char , il est autorisé par la norme d’utiliser une union avec des caractères unsigned char ou de convertir un pointeur sur unsigned char * et d’accéder ainsi à un object de données arbitraire. . Si vous écrivez ces informations sur le disque, elles ne seront pas portables vers d’autres systèmes utilisant des représentations différentes, mais cela pourrait néanmoins être utile au moment de l’exécution, par exemple, l’implémentation d’une table de hachage pour stocker double valeurs double . (Quelqu’un veut me corriger si des problèmes de bourrage rendent cette technique invalide?) Si rien d’autre, elle peut être utilisée pour implémenter memcpy (pas très utile car la bibliothèque standard vous fournit une bien meilleure implémentation) ou (plus intéressant) une fonction memswap qui pourrait échanger deux objects de taille arbitraire avec un espace temporaire délimité. Cela a eu pour effet de placer un peu en dehors du domaine d’utilisation des syndicats maintenant et de le unsigned char * dans un territoire de dissortingbution de personnages unsigned char * , mais cela est étroitement lié.

L’une des façons d’utiliser les syndicats que j’ai rencontrés consiste à cacher des données.

Disons que vous avez une structure qui est le tampon

ensuite, en autorisant l’union sur la structure de certains modules, vous pouvez accéder au contenu du tampon de différentes manières ou pas du tout, en fonction de l’union déclarée dans ce module particulier.

EDIT: voici un exemple

 struct X { int a; }; struct Y { int b; }; union Public { struct X x; struct Y y; }; 

celui qui utilise l’union XY peut convertir XY en struct X ou Y

donc donné une fonction:

 void foo(Public* arg) { ... 

vous pouvez accéder à la fois à la structure X ou à la structure Y

mais alors vous voulez limiter l’access pour que l’utilisateur ne connaisse pas X

le nom de l’union rest le même mais la partie struct X n’est pas disponible (via l’en-tête)

 void foo(Public* arg) { // Public is still available but struct X is gone, // user can only cast to struct Y struct Y* p = (struct Y*)arg; ... 

L’utilisation d’une union pour le type Punning n’est pas portable (bien que pas particulièrement moins portable que toute autre méthode de type Punning).

OTOH, un parsingur, par exemple, a généralement une union pour représenter les valeurs dans les expressions. [Edit: je remplace l’exemple d’parsing syntaxique par un exemple que j’espère un peu plus compréhensible]:

Considérons un fichier de ressources Windows. Vous pouvez l’utiliser pour définir des ressources telles que des menus, des boîtes de dialog, des icons, etc.

 #define mn1 2 mn1 MENU { MENUITEM "File", -1, MENUBREAK } ico1 "junk.ico" dlg1 DIALOG 100, 0, 0, 100, 100 BEGIN FONT 14, "Times New Roman" CAPTION "Test Dialog Box" ICON ico1, 700, 20, 20, 20, 20 TEXT "This is a ssortingng", 100, 0, 0, 100, 10 LTEXT "This is another ssortingng", 200, 0, 10, 100, 10 RTEXT "Yet a third ssortingng", 300, 0, 20, 100, 10 LISTBOX 400, 20, 20, 100, 100 CHECKBOX "A combobox", 500, 100, 100, 200, 10 COMBOBOX 600, 100, 210, 200, 100 DEFPUSHBUTTON "OK", 75, 200, 200, 50, 15 END 

L’parsing d’un MENU donne une définition de menu; l’parsing du DIALOGUE donne une définition de dialog et ainsi de suite. Dans l’parsingur, nous représentons cela en tant que syndicat:

 %union { struct control_def { char window_text[256]; int id; char *class; int x, y, width, height; int ctrl_style; } ctrl; struct menu_item_def { char text[256]; int identifier; } item; struct menu_def { int identiifer; struct menu_item_def items[256]; } mnu; struct font_def { int size; char filename[256]; } font; struct dialog_def { char caption[256]; int id; int x, y, width, height; int style; struct menu_def *mnu; struct control_def ctrls[256]; struct font_def font; } dlg; int value; char text[256]; }; 

Ensuite, nous spécifions le type qui sera produit en analysant un type d’expression particulier. Par exemple, une définition de police dans le fichier devient un membre de font de l’union:

 %type  font 

Juste pour clarifier, la partie fait référence au membre du syndicat qui est produit et la deuxième “police” se réfère à une règle d’parsing qui générera un résultat de ce type. Voici la règle pour ce cas particulier:

 font: T_FONT T_NUMBER "," T_STRING { $$.size = $2; strcpy($$.filename,$4); }; 

Oui, en théorie, nous pourrions utiliser ici une structure plutôt qu’une union – mais au-delà du gaspillage de mémoire, cela n’a aucun sens. Une définition de police dans le fichier définit uniquement une police. Cela n’aurait aucun sens de le faire produire une structure incluant une définition de menu, une définition d’icône, un nombre, une chaîne, etc., en plus de la police réellement définie. [fin de l’édition]

Bien sûr, utiliser les unions pour économiser de la mémoire est rarement très important. Bien que cela puisse sembler plutôt sortingvial à l’heure actuelle, alors que 64 Ko de RAM étaient considérables, les économies de mémoire signifiaient bien davantage.

Considérons un registre de contrôle matériel avec différents champs de bits. En définissant des valeurs dans ces champs de bits des registres, nous pouvons contrôler différentes fonctionnalités du registre.

En utilisant le type de données Union, vous pouvez soit modifier tout le contenu du registre, soit un champ de bits particulier du registre.

Pour Ex: considérons un type de données d’union comme suit,

 /* Data1 Bit Defintion */ typedef union { struct STRUCT_REG_DATA { unsigned int u32_BitField1 : 3; unsigned int u32_BitField2 : 2; unsigned int u32_BitField3 : 1; unsigned int u32_BitField4 : 2; } st_RegData; unsigned int u32_RegData; } UNION_REG_DATA; 

Pour modifier tout le contenu du registre,

 UNION_REG_DATA un_RegData; un_RegData. u32_RegData = 0x77; 

Pour modifier le contenu du champ de bit unique (For Ex Bitfield3)

 un_RegData.st_RegData.u32_BitField3 = 1; 

Les deux reflètent dans la même mémoire. Ensuite, cette valeur peut être écrite dans la valeur du registre de contrôle matériel.

Voici un exemple pratique:

Il existe des microcontrôleurs dans lesquels leurs mémoires non volatiles stockent les données en blocs d’octets. Comment pourriez-vous facilement stocker un tableau de flotteurs dans ces souvenirs? Nous soaps qu’en C, les floats ont une longueur de 32 bits (4 octets), donc:

 union float_uint8 { uint8 i[KNFLOATS*4]; //or KNFLOATS*sizeof(float) float f[KNFLOATS]; }; 

Vous pouvez maintenant stocker / adresser des éléments flottants avec des variables / pointeurs de type float_uint8 et avec une boucle, vous pouvez facilement les stocker en mémoire sous forme d’octets décomposés sans effectuer de conversion ou de décomposition. Et la même histoire se répète lors de la lecture de la mémoire. Même vous n’avez pas besoin de savoir comment les flottants sont décomposés en octets pour stocker ou récupérer les données stockées en mémoire.

Cet exemple est extrait de mon propre travail. Alors oui, ils sont utiles.