Union' punning 'structs w / " common initial sequence": pourquoi C (99+), mais pas C++, stipule-t-il une 'déclaration visible du type union'?

Contexte

Les Discussions sur la nature essentiellement non définie ou définie par l'implémentation du punning de type via un union citent généralement les bits suivants, ici via @ecatmur ( https://stackoverflow.com/a/31557852/2757035 ), sur une exemption pour la norme-cadre struct s ayant une "séquence initiale commune" de types de membres:

C11 (6.5.2.3 structure et membres syndicaux; sémantique):

[...] si une union contient plusieurs structures qui partagent une séquence initiale commune (Voir ci-dessous), et si l'objet union contient l'une de ces structures, il est permis d'inspecter le partie initiale commune de l'un d'entre eux partout où une déclaration de le type complété de l'union est visible. Deux structures partagent un séquence initiale commune si les membres correspondants ont des types compatibles (et, pour les champs de bits, les mêmes largeurs) pour une séquence d'un ou plus initiale membre.

C++03 ([classe.mem]/16):

Si un pod-union contient deux structures de POD ou plus qui partagent une séquence initiale commune, et si l'objet POD-union en contient actuellement une de ces structures POD, il est permis d'inspecter l'initiale commune une partie de l'un d'eux. Deux structures POD partagent une séquence initiale commune si les membres correspondants ont des types compatibles avec la mise en page (et, pour les mêmes largeurs) pour une séquence de une ou plusieurs initiales membre.

Les autres versions des deux standards ont un langage similaire; depuis C++11 la terminologie utilisée est standard-layout plutôt que POD.

Comme aucune réinterprétation n'est requise, ce n'est pas vraiment un jeu de type, juste une substitution de nom appliquée aux accès aux membres union. Une proposition pour C++17 (l'infâme P0137R1) rend cela explicite en utilisant un langage comme 'l'accès est comme si l'autre membre struct était désigné'.

Mais veuillez noter le gras - " anywhere that a declaration of the completed type of the union is visible" - une clause qui existe en C11 mais nulle part dans les ébauches C++ pour 2003, 2011 ou 2014 (toutes presque identiques, mais les versions ultérieures remplacent " POD " par le nouveau terme standard layout ). Dans tous les cas, la déclaration visible du bit de type union est totalement absente dans la section correspondante de tout standard C++.

@ loop et @ Mints97, ici - https://stackoverflow.com/a/28528989/2757035 - montrer que cette ligne était également absente en C89, apparaissant d'abord en C99 et restant en C depuis lors (bien que, encore une fois, ne filtrant jamais en C++).

Discussions sur les normes à ce sujet

[ciselée - voir ma réponse]

Questions

À partir de là, mes questions étaient:

  • Qu'est-ce que cela signifie? Qu'est-ce qui est classé comme une "déclaration visible"? Cette clause a pour but de restreindre - ou élargir - la gamme des contextes dans lesquels un tel "punning"a défini un comportement?

  • Devons-nous supposer que cette omission en C++ est très délibérée?

  • Quelle est la raison pour laquelle C++ diffère de C? Est-ce que C++ a juste "hérité" de C89 et ensuite décider-ou pire, oublier - de mettre à jour à côté de C99?

  • Si la différence est intentionnelle, alors quels sont les avantages ou les inconvénients aux 2 traitements différents en C vs c++?

  • Quelles ramifications intéressantes, le cas échéant, ont-elles à la compilation ou à l'exécution? par exemple, @ecatmur, dans un commentaire répondant à mon fait remarquer ceci sur sa réponse originale (lien comme ci-dessus), a spéculé comme suit.

J'imagine que cela permet une optimisation plus agressive; C peut supposer que les arguments de fonction S* s et T* t n'alias pas même s'ils partagent un séquence initiale commune comme aucun union { S; T; } est en vue, alors que C++ peut faire cette hypothèse seulement au moment du lien. Ça pourrait valoir la peine poser une question distincte sur cette différence.

Eh bien, me voici, demandant! Je suis très intéressé par toutes les pensées à ce sujet, en particulier: d'autres parties pertinentes de la norme (soit), des citations de membres du comité ou d'autres commentateurs estimés, des idées de développeurs qui auraient pu remarquer une différence pratique à cause de cela - en supposant que tout compilateur même dérange pour appliquer la clause ajoutée de C-et etc. L'objectif est de générer un catalogue utile de faits pertinents sur cette clause C et son omission (intentionnelle ou non) de C++. Alors, allons-y!

22
demandé sur Community 2016-01-05 19:06:09

2 réponses

J'ai trouvé mon chemin à travers le labyrinthe à quelques grandes sources sur ce sujet, et je pense que j'ai un résumé assez complet de celui-ci. Je poste ceci comme une réponse parce qu'il semble expliquer à la fois l'intention (très erronée) de la clause C et le fait que C++ ne l'hérite pas. Cela évoluera au fil du temps si je découvre d'autres documents à l'appui ou si la situation change.

C'est la première fois que j'essaie de résumer une situation très complexe, qui semble mal définie même pour de nombreux architectes linguistiques, je vais donc accueillir des clarifications / suggestions sur la façon d'améliorer cette réponse - ou simplement une meilleure réponse si quelqu'un en a une.

Enfin, quelques commentaires concrets

À travers des threads vaguement liés, j'ai trouvé la réponse suivante par @ tab-et j'ai beaucoup apprécié les liens contenus vers (éclairants, sinon concluants) GCC et les rapports de défaut du groupe de travail: réponse par tab sur StackOverflow

Le lien GCC contient quelques discussion et révèle une quantité importante de confusion et d'interprétations contradictoires de la part du Comité et des fournisseurs de compilateurs - entourant le sujet de union Membre structs, punning, et aliasing en C et c++.

À la fin de cela, nous sommes liés à l'événement principal - un autre thread de BugZilla, Bug 65892 , contenant unediscussion extrêmement utile. En particulier, nous trouvons notre chemin vers le premier de deux documents pivots:

Origine du produit ajouté ligne en C99

C proposition N685 est l'origine de la clause ajoutée concernant la visibilité d'une déclaration de type union. Par ce que certains prétendent (voir GCC thread #2) est une mauvaise interprétation totale de l'allocation "common initial sequence", N685 était en effet destiné à permettre l'assouplissement des règles d'aliasing pour" Common initial sequence " structs au sein D'un TU conscient de certains union contenant des instances desdits struct types , Comme nous pouvons le voir]}

La solution proposée consiste à exiger qu'une déclaration syndicale soit visible si les alias à travers une séquence initiale commune (comme ci-dessus) sont possibles. Par conséquent, le TU suivant fournit ce type d'aliasing si désiré:

union utag {
    struct tag1 { int m1; double d2; } st1;
    struct tag2 { int m1; char c2; } st2;
};

int similar_func(struct tag1 *pst2, struct tag2 *pst3) {
     pst2->m1 = 2;
     pst3->m1 = 0;   /* might be an alias for pst2->m1 */
     return pst2->m1;
}

A en juger par la discussion du GCC et les commentaires ci-dessous tels que ceux de @ecatmur, cette proposition-qui semble exiger de manière spéculative l'autorisation d'aliasing pour tout type struct qui a une instance dans certains union visible à ce TU- semble avoir reçu une grande dérision et rarement été mis en œuvre .

Il est évident à quel point il serait difficile de satisfaire cette interprétation de la clause ajoutée sans paralyser totalement de nombreuses optimisations-pour peu d'avantages, car peu de codeurs voudraient cette garantie, et ceux qui le font peuvent simplement activer fno-strict-aliasing (ce qui indique des problèmes plus importants). Si elle est mise en œuvre, cette allocation est plus susceptible d'attraper les gens et d'interagir avec d'autres déclarations de union s, que d'être utile.

Omission de la ligne de C++

Suite à ceci et à un commentaire que j'ai fait ailleurs, @ Potatoswatter dans cette réponse ici sur SO déclare que:

La partie visibilité a été volontairement omise de C++ car elle est largement considérée comme ridicule et irréprochable.

En d'autres termes, Il semble que C++ ait délibérément évité d'adopter cette clause ajoutée, probablement en raison de son absurdité largement perçue. sur demande pour une citation "sur le dossier" de cela, Potatoswatter a fourni les informations clés suivantes sur les participants du fil:

Les gens dans cette discussion sont essentiellement" sur le dossier " là. Andrew Pinski est un gars Hardcore GCC backend. Martin Sebor est un membre actif du Comité C. Jonathan Wakely est un membre actif du Comité C++ et implémenteur de langage / bibliothèque. Cette page est plus autoritaire, claire et complète que tout ce que je pouvais écrire.

Potatoswatter, dans le même thread SO lié ci-dessus, conclut que C++ a délibérément exclu cette ligne, ne laissant aucun traitement spécial (ou, au mieux, un traitement défini par l'implémentation) pour les pointeurs dans la séquence initiale commune. Si leur traitement sera à l'avenir spécifiquement défini, par rapport à tout autre pointeur, reste à voir; comparez à ma dernière section ci-dessous à propos de C. à l'heure actuelle, cependant, ce n'est pas le cas (et encore une fois, IMO, c'est bon).

Qu'est-ce que c'est moyenne pour les implémentations C++ et c pratiques?

Donc, avec la ligne infâme de N685... "jeter de côté"... nous revenons à supposer que les pointeurs dans la séquence initiale commune ne sont pas spéciaux en termes d'aliasing. Encore. cela vaut la peine de confirmer ce que signifie ce paragraphe en C++ sans cela. Eh bien, le 2ème fil GCC ci-dessus est lié à une autre gemme:

Défaut c++ 1719. Cette proposition a atteint le statut DRWP : "une question de RD dont la résolution est reflétée dans l'actuel Document de Travail. Le Document de Travail est un projet pour une future version de la Norme" - cite. Ceci est soit post C++14 ou au moins après le projet final que j'ai ici (N3797) - et met en avant une significative, et à mon avis éclairante, réécriture du libellé de ce paragraphe , comme suit. Je mets en gras ce que je considère comme les changements importants, et {ces commentaires} sont les miens:

Dans une norme-cadre membre {"actif" indique un union exemple, non seulement le type de} (9.5 [classe.Union]) de type struct T1, il est permis de lire {anciennement "inspecter"} un non-membre de données statiques m d'un autre membre de l'union de type struct T2 à condition que m fasse partie du séquence initiale commune de T1 et T2. [Note : lecture d'un objet volatile grâce à un glvalue non volatile a un comportement indéfini (7.1.6.1 [dcl.type.cv]). - fin remarque]

Cela semble clarifier le sens de l'ancienne formulation: pour moi, il dit que tout 'punning' spécifiquement autorisé parmi union Membre structS avec des séquences initiales communes doit être fait via une instance du parent union - plutôt que d'être basé sur le type de structs (par exemple, des pointeurs vers eux sont passés à une fonction). Cette formulation semble exclure toute autre interprétation, la N685. C ferait bien d'adopter ceci, je dirais. Hey, parlant de qui, voir ci-dessous!

Le résultat est que-comme bien démontré par @ecatmur et dans les tickets GCC - cela laisse Tel union Membre struct s par définition en C++, et pratiquement en C, soumis aux mêmes règles d'aliasing strictes que tout autre 2 pointeurs officiellement indépendants.{[42] } la garantie explicite de pouvoir lire la séquence initiale commune des membres inactifs union struct s est maintenant plus clairement définie, sans compter la "visibilité" vague et inimaginablement fastidieuse à appliquer" comme tenté par N685 pour C. par cette définition, les compilateurs principaux se sont comportés comme prévu pour C++. Comme pour le C?

Inversion Possible de cette ligne en C / clarification en C++

Il est également très intéressant de noter que le membre du Comité C, Martin Sebor, cherche également à corriger cela dans ce beau langage:

Martin Sebor 2015-04-27 14: 57: 16 UTC Si l'un d'entre vous peut expliquer le problème avec elle, je suis prêt à écrire un document et le soumettre WG14 et demande de modification de la norme.

Martin Sebor 2015-05-13 16:02: 41 UTC j'ai eu la chance de discuter de cette question avec Clark Nelson la semaine dernière. Clark a travaillé sur l'amélioration des parties d'aliasing de la spécification C dans le passé, par exemple dans N1520 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm ). Il a convenu que, comme les questions soulignées dans N1520, il s'agit également d'un problème en suspens qui mériterait que le GT14 revoie et fixer."

Potatoswatter conclut avec inspiration:

Les comités C et c++ (Via Martin et Clark) vont essayer de trouver un consensus et marteler le libellé afin que la norme puisse enfin dire ce que cela signifie.

Nous ne pouvons qu'espérer!

Encore une fois, toutes les autres pensées sont les bienvenues.

14
répondu underscore_d 2017-05-23 12:08:47

Je soupçonne que cela signifie que l'accès à ces parties communes est autorisée, non seulement par le type d'union, mais à l'extérieur de l'union. Admettons que nous avons ceci:

union u {
  struct s1 m1;
  struct s2 m2;
};

Supposons maintenant que dans une fonction, nous avons un pointeur struct s1 *p1 que nous savons avoir été levé du Membre m1 d'une telle union. Nous pouvons convertir cela en un pointeur struct s2 * et accéder toujours aux membres qui sont en commun avec struct s1. Mais quelque part dans la portée, une déclaration de union u doit être visible. Et il doit être la déclaration complète, qui informe le compilateur que les membres sont struct s1 et struct s2.

L'intention probable est que s'il y a un tel type dans la portée, alors le compilateur sait que struct s1 et struct s2 sont Alias, et donc un accès via un pointeur struct s1 * est suspecté d'accéder réellement à un struct s2 ou vice versa.

En l'absence de tout type d'union visible qui rejoint ces types de cette façon, il n'y a pas une telle connaissance; un aliasing strict peut être appliqué.

Depuis le le libellé est absent de C++, alors pour profiter de la règle "common initial members relaxation" dans cette langue, vous devez router les accès via le type union, comme cela se fait généralement de toute façon:

union u *ptr_any;
// ...
ptr_any->m1.common_initial_member = 42;
fun(ptr_any->m2.common_initial_member);  // pass 42 to fun
6
répondu Kaz 2016-01-05 16:47:34