Les syndicats et les types

J’ai cherché pendant un moment, mais je ne trouve pas de réponse claire.

Beaucoup de gens disent que l’utilisation des syndicats pour taper des mots n’est pas définie et est une mauvaise pratique. Pourquoi est-ce? Je ne vois aucune raison pour laquelle il ferait quoi que ce soit d’indéfini compte tenu de la mémoire dans laquelle vous écrivez les informations d’origine, car il ne va pas simplement changer (sauf si la stack ne fait pas partie de la scope) , ce serait une mauvaise conception).

Les gens citent la règle de l’aliasing ssortingcte, mais cela me semble être comme dire que vous ne pouvez pas le faire parce que vous ne pouvez pas le faire.

De plus, quel est le but d’un syndicat si ce n’est de taper un jeu de mots? J’ai vu quelque part qu’ils étaient censés être utilisés pour utiliser le même emplacement de mémoire pour des informations différentes à des moments différents, mais pourquoi ne pas simplement supprimer les informations avant de les réutiliser?

Pour résumer:

  1. Pourquoi est-il mauvais d’utiliser des syndicats pour des types de punition?
  2. Qu’est-ce que le point d’eux si ce n’est pas cela?

Informations supplémentaires: J’utilise principalement le C ++, mais j’aimerais savoir à ce sujet et C. Plus précisément, j’utilise les unions pour convertir entre les flottants et l’hex brut pour envoyer via le bus CAN.

Pour réitérer, la punition de type à travers les unions est parfaitement correcte en C (mais pas en C ++). En revanche, l’utilisation de pointeurs pour le faire viole le crénelage ssortingct C99 et pose problème car différents types peuvent avoir des exigences d’alignement différentes et vous pouvez générer un SIGBUS si vous le faites mal. Avec les syndicats, ce n’est jamais un problème.

Les citations pertinentes des normes C sont:

C89 section 3.3.2.3 §5:

si un membre d’un object union est accédé après qu’une valeur a été stockée dans un autre membre de l’object, le comportement est défini par l’implémentation

C11 section 6.5.2.3 §3:

Une expression postfixe suivie par le. l’opérateur et un identifiant désignent un membre d’une structure ou d’un object d’union. La valeur est celle du membre nommé

avec la note de bas de page 95 suivante:

Si le membre utilisé pour lire le contenu d’un object union n’est pas identique au dernier membre utilisé pour stocker une valeur dans l’object, la partie appropriée de la représentation d’object de la valeur est réinterprétée comme une représentation d’object dans le nouveau type décrit en 6.2.6 (un processus parfois appelé «type punning»). Cela pourrait être une représentation de piège.

Cela devrait être parfaitement clair.


James est confus car C11 section 6.7.2.1 §16 lit

La valeur d’au plus un des membres peut être stockée dans un object d’union à tout moment.

Cela semble contradictoire, mais ce n’est pas le cas: contrairement à C ++, en C, il n’y a pas de concept de membre actif et il est parfaitement acceptable d’accéder à la valeur stockée unique via une expression d’un type incompatible.

Voir aussi annexe C11, J.1 §1:

Les valeurs des octets qui correspondent aux membres d’union autres que ceux stockés en dernier dans [ne sont pas spécifiées].

En C99, ceci lisait

La valeur d’un membre d’union autre que le dernier stocké dans [n’est pas spécifiée]

C’était incorrect. Comme l’annexe n’est pas normative, elle n’a pas évalué son propre TC et a dû attendre la prochaine révision standard pour être corrigée.


Les extensions GNU au standard C ++ (et au C90) autorisent explicitement le “type-punning” avec les unions . Les autres compilateurs qui ne prennent pas en charge les extensions GNU peuvent également prendre en charge le “punition de type union”, mais cela ne fait pas partie du standard du langage de base.

L’objective initial des syndicats était de gagner de la place lorsque vous souhaitez pouvoir représenter différents types, ce que nous appelons un type de variante, voir Boost.Variant comme un bon exemple.

L’autre usage courant est le type punition de la validité de ce qui est débattu mais pratiquement la plupart des compilateurs le supportent, nous pouvons voir que gcc documente son support :

La pratique de lire à partir d’un autre membre de l’union que celui auquel on a récemment écrit (appelée «punition de type») est courante. Même avec -fssortingct-aliasing, le type-punning est autorisé, à condition que la mémoire soit accessible via le type d’union. Ainsi, le code ci-dessus fonctionne comme prévu.

Notez que même avec -fssortingct-aliasing, le type-punning est autorisé, ce qui indique qu’il existe un problème d’alias en jeu.

Pascal Cuoq a fait valoir que le rapport de défaut 283 précisant que cela était autorisé dans le rapport C. Defect 283 a ajouté la note de bas de page suivante à titre de clarification:

Si le membre utilisé pour accéder au contenu d’un object union n’est pas identique au dernier membre utilisé pour stocker une valeur dans l’object, la partie appropriée de la représentation d’object de la valeur est réinterprétée comme une représentation d’object dans le nouveau type décrit en 6.2.6 (un processus parfois appelé “type punning”). Cela pourrait être une représentation de piège.

en C11, ce serait la note de bas de page 95 .

Bien que dans le sujet du groupe de std-discussion Type Punning via une union, l’argument est avancé, ceci est sous-spécifié, ce qui semble raisonnable puisque DR 283 n’a pas ajouté de nouvelle formulation normative, juste une note:

C’est, à mon avis, un bourbier sémantique sous-spécifié dans C. Le consensus n’a pas été atteint entre les exécutants et le comité C quant aux cas précis qui ont défini un comportement et qui ne le […]

En C ++, il est difficile de savoir si le comportement est défini ou non .

Cette discussion couvre également au moins une raison pour laquelle il est indésirable d’autoriser la punition de type via une union:

[…] les règles du standard C rompent les optimisations d’parsing par alias basées sur les types que les implémentations actuelles effectuent.

cela casse certaines optimisations. Le second argument contre cela est que l’utilisation de memcpy devrait générer un code identique et ne rompt pas les optimisations et les comportements bien définis, par exemple:

 std::int64_t n; std::memcpy(&n, &d, sizeof d); 

au lieu de cela:

 union u1 { std::int64_t n; double d ; } ; u1 u ; ud = d ; 

et nous pouvons voir en utilisant godbolt que cela génère du code identique et que l’argument est fait si votre compilateur ne génère pas un code identique, il devrait être considéré comme un bogue:

Si cela est vrai pour votre implémentation, je vous suggère de lui envoyer un bogue. Briser les optimisations réelles (tout ce qui est basé sur l’parsing des alias basés sur les types) afin de contourner les problèmes de performances avec un compilateur particulier semble être une mauvaise idée pour moi.

Le post de blog Type Punning, Ssortingct Aliasing et Optimization arrive également à une conclusion similaire.

La discussion sur les comportements indéfinis: la frappe de type pour éviter la copie couvre une grande partie du même terrain et nous pouvons voir à quel point le territoire peut être gris.

C’est légal en C99:

De la norme: 6.5.2.3 Structure et membres du syndicat

Si le membre utilisé pour accéder au contenu d’un object union n’est pas identique au dernier membre utilisé pour stocker une valeur dans l’object, la partie appropriée de la représentation d’object de la valeur est réinterprétée comme une représentation d’object dans le nouveau type décrit en 6.2.6 (un processus parfois appelé “type punning”). Cela pourrait être une représentation de piège.

BREF REPONSE: La punition de type peut être sûre dans quelques circonstances. D’un autre côté, même si cela semble être une pratique très connue, il semble que la norme ne soit pas très intéressée à la rendre officielle.

Je ne parlerai que de C (pas de C ++).

1. TYPE PUNNING ET LES NORMES

Comme les personnes l’ont déjà indiqué mais que la punition de type est autorisée dans la norme C99 et aussi dans C11, dans la sous-section 6.5.2.3 . Cependant, je vais réécrire les faits avec ma propre perception du problème:

  • La section 6.5 des documents standard C99 et C11 développe le sujet des expressions .
  • La sous-section 6.5.2 fait référence aux expressions postfixes .
  • La sous-sous-section 6.5.2.3 concerne les structures et les unions .
  • Le paragraphe 6.5.2.3 (3) explique l’ opérateur point appliqué à un object struct ou union et quelle valeur sera obtenue.
    Juste là, la note de bas de page 95 apparaît. Cette note de bas de page dit:

Si le membre utilisé pour accéder au contenu d’un object union n’est pas identique au dernier membre utilisé pour stocker une valeur dans l’object, la partie appropriée de la représentation d’object de la valeur est réinterprétée comme une représentation d’object dans le nouveau type décrit en 6.2.6 (un processus parfois appelé “type punning”). Cela pourrait être une représentation de piège.

Le fait que ce type de punition apparaisse à peine, et comme note de bas de page, donne un indice que ce n’est pas un problème pertinent dans la programmation en langage C.
En réalité, le principal objective de l’utilisation des unions est d’économiser de l’espace (en mémoire). Comme plusieurs membres partagent la même adresse, si l’on sait que chaque membre sera utilisé dans différentes parties du programme, jamais au même moment, une union peut être utilisée à la place d’une struct , pour économiser de la mémoire.

  • La sous-section 6.2.6 est mentionnée.
  • La sous-section 6.2.6 explique comment les objects sont représentés (en mémoire, par exemple).

2. REPRÉSENTATION DES TYPES ET DE SON PROBLÈME

Si vous prêtez attention aux différents aspects de la norme, vous pouvez être sûr de ne presque rien:

  • La représentation des pointeurs n’est pas clairement spécifiée.
  • Pire, les pointeurs ayant différents types pourraient avoir une représentation différente (en tant qu’objects en mémoire).
  • union membres d’ union partagent la même adresse de titre en mémoire, et c’est la même adresse que l’object union lui-même.
  • struct membres de struct ont une adresse relative croissante, en démarrant exactement à la même adresse mémoire que l’object struct lui-même. Cependant, des octets de remplissage peuvent être ajoutés à la fin de chaque membre. Combien? C’est imprévisible. Les octets de remplissage sont principalement utilisés à des fins d’allocation de mémoire.
  • Les types arithmétiques (nombres entiers, nombres réels et complexes à virgule flottante) peuvent être représentables de différentes manières. Cela dépend de l’implémentation.
  • En particulier, les types entiers peuvent avoir des bits de remplissage . Je pense que ce n’est pas vrai pour les ordinateurs de bureau. Cependant, la norme laissait la porte ouverte à cette possibilité. Les bits de remplissage sont utilisés à des fins spéciales (parité, signaux, qui sait) et non pour conserver des valeurs mathématiques.
  • signed types signed peuvent avoir 3 manières d’être représentés: complément à 1, complément à 2, simplement bit de signe.
  • Les types de caractères occupent seulement 1 octet, mais 1 octet peut avoir un nombre de bits différent de 8 (mais jamais moins de 8).
  • Cependant, nous pouvons être certains de certains détails:

    une. Les types de caractères n’ont pas de bits de remplissage.
    b. Les types entiers unsigned sont représentés exactement comme sous forme binary.
    c. unsigned char occupe exactement 1 octet, sans bits de remplissage, et il n’y a pas de représentation de piège car tous les bits sont utilisés. De plus, il représente une valeur sans ambiguïté, suivant le format binary des nombres entiers.

3. TYPE PUNNING vs REPRÉSENTATION DE TYPE

Toutes ces observations révèlent que, si nous essayons de faire une frappe de type avec des membres d’ union ayant des types différents de caractères unsigned char , nous pourrions avoir beaucoup d’ambiguïté. Ce n’est pas un code portable et, en particulier, nous pourrions avoir un comportement imprévisible de notre programme.
Cependant, la norme autorise ce type d’access .

Même si nous sums sûrs de la manière spécifique dont chaque type est représenté dans notre implémentation, nous pourrions avoir une séquence de bits qui ne signifie rien du tout dans les autres types ( représentation des pièges ). Nous ne pouvons rien faire dans ce cas.

4. LE CAS SÉCURITAIRE: char non signé

La seule manière sûre d’utiliser le type punning est d’utiliser des tableaux unsigned char unsigned char ou unsigned char (car nous soaps que les membres des objects tableau sont ssortingctement contigus et qu’il n’y a pas d’octets de remplissage lorsque sizeof() ).

  union { TYPE data; unsigned char type_punning[sizeof(TYPE)]; } xx; 

Comme nous soaps que le caractère unsigned char est représenté sous forme binary ssortingcte, sans bits de remplissage, le type punning peut être utilisé ici pour examiner la représentation binary des data du membre.
Cet outil peut être utilisé pour parsingr comment les valeurs d’un type donné sont représentées, dans une implémentation particulière.

Je ne suis pas en mesure de voir une autre application sûre et utile du type punning selon les spécifications standard.

5. UN COMMENTAIRE SUR LES CASTS …

Si l’on veut jouer avec les types, il est préférable de définir vos propres fonctions de transformation, ou bien d’utiliser simplement des moulages . Nous pouvons nous souvenir de cet exemple simple:

  union { unsigned char x; double t; } uu; bool result; uu.x = 7; (uu.t == 7.0)? result = true: result = false; // You can bet that result == false uu.t = (double)(uu.x); (uu.t == 7.0)? result = true: result = false; // result == true 

Il y a (ou du moins en arrière dans C90) deux modivations pour rendre ce comportement indéfini. La première était qu’un compilateur serait autorisé à générer du code supplémentaire qui suivait ce qui était dans l’union, et générait un signal lorsque vous accédiez au mauvais membre. En pratique, je ne pense pas que quelqu’un ait jamais fait (peut-être CenterLine?). L’autre était les possibilités d’optimisation qui s’ouvraient et celles-ci sont utilisées. J’ai utilisé des compilateurs qui retarderaient une écriture jusqu’au dernier moment, sous prétexte que cela pourrait ne pas être nécessaire (parce que la variable sort du cadre ou qu’il y a une écriture ultérieure d’une valeur différente). Logiquement, on pourrait s’attendre à ce que cette optimisation soit désactivée lorsque l’union est visible, mais elle ne se trouve pas dans les premières versions de Microsoft C.

Les problèmes de type punning sont complexes. Le comité C (à la fin des années 1980) a plus ou moins pris la position que vous devriez utiliser des casts (en C ++, reinterpret_cast) pour cela, et non des syndicats, bien que les deux techniques étaient très répandues à l’époque. Depuis lors, certains compilateurs (g ++, par exemple) ont adopté le sharepoint vue opposé, soutenant l’utilisation des unions, mais pas l’utilisation de moulages. Et dans la pratique, ni l’un ni l’autre ne fonctionnent s’il n’est pas immédiatement évident qu’il y a des punitions de type. Cela pourrait être la motivation derrière le sharepoint vue de g ++. Si vous accédez à un membre du syndicat, il est immédiatement évident qu’il pourrait y avoir des punitions de type. Mais bien sûr, quelque chose comme:

 int f(const int* pi, double* pd) { int results = *pi; *pd = 3.14159; return results; } 

appelé avec:

 union U { int i; double d; }; U u; ui = 1; std::cout << f( &u.i, &u.d ); 

est parfaitement légal selon les règles ssortingctes de la norme, mais échoue avec g ++ (et probablement beaucoup d'autres compilateurs); en compilant f , le compilateur suppose que pi et pd ne peuvent pas créer d'alias, et réordonne l'écriture dans *pd et la lecture depuis *pi . (Je crois que cela n'a jamais été l'intention que cela soit garanti. Mais le libellé actuel de la norme le garantit.)

MODIFIER:

Étant donné que d'autres réponses ont fait valoir que le comportement est en fait défini (largement basé sur la citation d'une note non normative, prise hors contexte):

La réponse correcte ici est celle de pablo1977: la norme ne tente pas de définir le comportement quand un type de punition est impliqué. La raison probable est qu'il n'y a pas de comportement portable qu'il pourrait définir. Cela n'empêche pas une implémentation spécifique de la définir; Bien que je ne me souvienne pas de discussions spécifiques sur le problème, je suis sûr que l’intention était que les implémentations définissent quelque chose (et la plupart, sinon toutes).

En ce qui concerne l'utilisation d'une union pour le typage: lorsque le comité C développait C90 (à la fin des années 1980), il y avait une intention claire d'autoriser les implémentations de débogage qui effectuaient des vérifications supplémentaires. D'après les discussions de l'époque, il était clair qu'une implémentation de débogage pouvait mettre en cache des informations concernant la dernière valeur initialisée dans une union et intercepter si vous tentiez d'accéder à autre chose. Ceci est clairement indiqué au § 6.7.2.1 / 16: "La valeur d'au plus un des membres peut être stockée dans un object d'union à tout moment." L'access à une valeur qui n'est pas là est un comportement indéfini; il peut être assimilé à l'access à une variable non initialisée. (Il y a eu des discussions à l'époque pour savoir si l'access à un membre différent du même type était légal ou non. Je ne sais pas quelle était la résolution finale, mais vers 1990, je suis passé à C ++.)

En ce qui concerne la citation de C89, dire que le comportement est défini par la mise en œuvre: le trouver dans la section 3 (Termes, définitions et symboles) semble très étrange. Je vais devoir chercher dans mon exemplaire de C90 à la maison; le fait qu'il ait été supprimé dans des versions ultérieures des normes suggère que sa présence était considérée comme une erreur par le comité.

L'utilisation des unions supscopes par la norme est un moyen de simuler la dérivation. Vous pouvez définir:

 struct NodeBase { enum NodeType type; }; struct InnerNode { enum NodeType type; NodeBase* left; NodeBase* right; }; struct ConstantNode { enum NodeType type; double value; }; // ... union Node { struct NodeBase base; struct InnerNode inner; struct ConstantNode constant; // ... }; 

et accéder légalement à base.type, même si le noeud a été initialisé par l' inner . (Le fait que le § 6.5.2.3 / 6 commence par "Une garantie spéciale est faite ..." et continue explicitement à autoriser ceci est une indication très forte que tous les autres cas sont censés être un comportement indéfini. Et bien sûr, il y a est l'affirmation que "le comportement indéfini est autrement indiqué dans la présente Norme internationale par les mots" comportement indéfini "ou par l'omission de toute définition explicite du comportement " dans le §4 / 2; afin d'affirmer que le comportement n'est pas indéfini , vous devez montrer où il est défini dans la norme.)

Enfin, en ce qui concerne le type-punning: toutes les implémentations (ou du moins toutes celles que j'ai utilisées) le prennent en charge d'une manière ou d'une autre. Mon impression à l’époque était que l’intention était que le lancement de pointeur soit la manière dont une implémentation le supportait; dans le standard C ++, il y a même du texte (non normatif) pour suggérer que les résultats d'une reinterpret_cast soient "sans surprise" pour une personne familiarisée avec l'architecture sous-jacente. En pratique, cependant, la plupart des implémentations prennent en charge l’utilisation de l’union pour la punition de type, à condition que l’access passe par un membre du syndicat. La plupart des implémentations (mais pas g ++) prennent également en charge les conversions de pointeurs, à condition que la projection du pointeur soit clairement visible pour le compilateur (pour une définition non spécifiée de la dissortingbution du pointeur). Et la "standardisation" du matériel sous-jacent signifie que des choses comme:

 int getExponent( double d ) { return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023; } 

sont en fait assez portables. (Cela ne fonctionnera pas sur les mainframes, bien sûr.) Ce qui ne fonctionne pas, ce sont des choses comme mon premier exemple, où l'alias est invisible pour le compilateur. (Je suis quasiment certain que c'est un défaut de la norme. Je me souviens même d'avoir vu un DR le concernant.)