strutture di unione “punning” con “sequenza iniziale comune”: perché C (99+), ma non C ++, stabilisce una “dichiarazione visibile del tipo di unione”?

sfondo

Le discussioni sulla natura per lo più non implementata o definita del tipo punire tramite union tipicamente citano i seguenti bit, qui tramite @ecatmur ( https://stackoverflow.com/a/31557852/2757035 ), su un’esenzione per lo standard -struttura di visualizzazione con una “sequenza iniziale comune” di tipi di membri:

C11 ( 6.5.2.3 Struttura e membri del sindacato ; Semantica ):

[…] se un’unione contiene diverse strutture che condividono una sequenza iniziale comune (vedi sotto), e se l’object unione contiene attualmente una di queste strutture, è consentito ispezionare la parte iniziale comune di ognuna di esse ovunque la dichiarazione del tipo completato dell’unione è visibile . Due strutture condividono una sequenza iniziale comune se i membri corrispondenti hanno tipi compatibili (e, per i campi bit, le stesse larghezze) per una sequenza di uno o più membri iniziali.

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

Se un’unione POD contiene due o più strutture POD che condividono una sequenza iniziale comune e se l’object POD-union contiene attualmente una di queste strutture POD, è consentito ispezionare la parte iniziale comune di ognuna di esse. Due strutture POD condividono una sequenza iniziale comune se i membri corrispondenti hanno tipi compatibili con il layout (e, per i campi bit, le stesse larghezze) per una sequenza di uno o più membri iniziali.

    Altre versioni dei due standard hanno un linguaggio simile; dal C ++ 11 la terminologia utilizzata è il layout standard piuttosto che il POD .

    Dal momento che non è richiesta alcuna reinterpretazione, non si tratta di punire tipicamente, basta sostituire il nome applicato agli accessi ai membri del union . Una proposta per C ++ 17 (il famigerato P0137R1) rende esplicito questo linguaggio usando “l’accesso è come se fosse stato nominato l’altro membro della struttura”.

    Ma si noti il ​​grassetto – ” ovunque sia visibile una dichiarazione del tipo completato dell’unione ” – una clausola che esiste in C11 ma da nessuna parte in bozze in C ++ per il 2003, il 2011 o il 2014 (tutti quasi identici, ma le versioni successive sostituiscono ” POD “con il nuovo layout standard di termine). In ogni caso, il bit ‘visible declaration of union type’ è totalmente assente nella corrispondente sezione di qualsiasi standard C ++.

    @loop e @ Mints97, qui – https://stackoverflow.com/a/28528989/2757035 – mostrano che questa linea era assente anche in C89, prima apparendo in C99 e rimanendo in C da allora (anche se, di nuovo, non filtra mai attraverso a C ++).

    Discussioni sugli standard attorno a questo

    [snipped – vedi la mia risposta]

    Domande

    Da questo, quindi, le mie domande erano:

    • Cosa significa questo? Cosa viene classificato come “dichiarazione visibile”? Questa clausola aveva lo scopo di restringere – o espandere – la gamma di contesti in cui tale “punire” ha definito il comportamento?

    • Dobbiamo supporre che questa omissione in C ++ sia molto deliberata?

    • Qual è la ragione per cui C ++ differisce da C? C ++ ha appena “ereditato” questo dalla C89 e quindi decide – o peggio, dimentica – di aggiornare insieme al C99?

    • Se la differenza è intenzionale, quali sono i vantaggi o gli svantaggi dei 2 diversi trattamenti in C vs C ++?

    • Quali sono le eventuali ramificazioni interessanti che ha in fase di compilazione o di runtime? Ad esempio, @ecatmur, in un commento che risponde al mio indicando questo sulla sua risposta originale (link come sopra), ha speculato come segue.

    Immagino che consenta un’ottimizzazione più aggressiva; C può assumere che gli argomenti di funzione S* s e T* t non siano alias anche se condividono una sequenza iniziale comune purché non vi sia alcuna union { S; T; } union { S; T; } union { S; T; } è in vista, mentre C ++ può fare questa ipotesi solo al momento del collegamento. Potrebbe valere la pena di fare una domanda a parte su questa differenza.

    Bene, eccomi, chiedendo! Sono molto interessato a qualsiasi idea al riguardo, in particolare: altre parti rilevanti dello Standard (o), citazioni da membri del comitato o altri stimati commentatori, intuizioni da parte di sviluppatori che potrebbero aver notato una differenza pratica a causa di questo – assumendo qualsiasi compilatore anche fastidio per far rispettare la clausola aggiuntiva di C – e così via. L’objective è quello di generare un utile catalogo di fatti rilevanti su questa clausola C e la sua omissione (intenzionale o meno) da C ++. Quindi andiamo!

    Ho trovato la mia strada attraverso il labirinto per alcune grandi fonti su questo, e penso di averne un sumrio piuttosto esaustivo. Sto postando questo come una risposta perché sembra spiegare sia l’intenzione (IMO molto fuorviata) della clausola C sia il fatto che C ++ non la erediti. Questo si evolverà nel tempo se scoprirò ulteriore materiale di supporto o cambiamenti di situazione.

    Questa è la prima volta che cerco di riassumere una situazione molto complessa, che sembra mal definita anche per molti architetti di lingue, quindi accoglierò chiarimenti / suggerimenti su come migliorare questa risposta – o semplicemente una risposta migliore se qualcuno ne ha uno.

    Infine, alcuni commenti concreti

    Tramite thread vagamente correlati, ho trovato la seguente risposta di @tab – e ho apprezzato molto i link contenuti a (illuminanti, se non conclusivi) rapporti sui difetti di GCC e Working Group: risposta per scheda su StackOverflow

    Il collegamento GCC contiene alcune discussioni interessanti e rivela una notevole quantità di confusione e interpretazioni contrastanti su parte del comitato e dei produttori di compilatori – che circonda l’argomento delle struct , puning e aliasing dei membri union in C e C ++.

    Alla fine, siamo collegati all’evento principale: un altro thread BugZilla, Bug 65892 , contenente una discussione estremamente utile. In particolare, troviamo la nostra strada verso il primo di due documenti cardine:

    Origine della riga aggiunta in C99

    La proposta N685 è l’origine della clausola aggiuntiva relativa alla visibilità di una dichiarazione del tipo di union . Attraverso ciò che alcuni sostengono (vedere il thread n. 2 di GCC) è un’interpretazione errata totale della tolleranza della “sequenza iniziale comune”, l’N685 era effettivamente inteso a consentire il rilassamento delle regole di aliasing per le strutture di “sequenza iniziale comune” all’interno di una TU consapevole di qualche union contenente istanze di detti tipi di struct , come possiamo vedere da questa citazione:

    La soluzione proposta è di richiedere che una dichiarazione di unione sia visibile se sono possibili alias attraverso una sequenza iniziale comune (come sopra). Pertanto la seguente TU fornisce questo tipo di aliasing se lo si desidera:

     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 giudicare dalla discussione e dai commenti del GCC di seguito come @ ecatmur, questa proposta – che sembra imporre speculativamente di consentire l’aliasing per qualsiasi tipo di struct che abbia qualche istanza all’interno di un union visibile a questa TU – sembra aver ricevuto grande derisione e raramente è stata implementata .

    È ovvio quanto sarebbe difficile soddisfare questa interpretazione della clausola aggiunta senza stravolgere totalmente le ottimizzazioni – per poco vantaggio, visto che pochi programmatori vorrebbero questa garanzia, e chi lo fa può semplicemente triggersre fno-strict-aliasing (che IMO indica problemi più grandi). Se implementata, questa indennità ha più probabilità di catturare le persone e interagire spurie con altre dichiarazioni di union , piuttosto che essere utile.

    Omissione della linea da C ++

    Seguendo questo e un commento che ho fatto altrove, @Potatoswatter in questa risposta qui su SO afferma che:

    La parte di visibilità è stata volutamente omessa dal C ++ perché è ampiamente considerata ridicola e non attuabile.

    In altre parole, sembra che C ++ abbia deliberatamente evitato di adottare questa clausola aggiuntiva, probabilmente a causa della sua assurdità ampiamente percepita. Chiedendo una citazione “on the record” di questo, Potatoswatter ha fornito le seguenti informazioni chiave sui partecipanti al thread:

    I membri di quella discussione sono essenzialmente “registrati” lì. Andrew Pinski è un hardcore backend GCC. Martin Sebor è un membro attivo del comitato C. Jonathan Wakely è un membro attivo del comitato C ++ e un implementatore linguistico / bibliotecario. Quella pagina è più autorevole, chiara e completa di qualsiasi altra cosa che potrei scrivere.

    Il potatoswatter, nello stesso thread SO linkato sopra, conclude che C ++ ha deliberatamente escluso questa linea, non lasciando alcun trattamento speciale (o, nel migliore dei casi, un trattamento definito dall’implementazione) per i puntatori nella sequenza iniziale comune. Resta da vedere se il loro trattamento sarà definito in futuro, rispetto a qualsiasi altro indicatore; confronta la mia ultima sezione qui sotto su C. Al momento, però, non lo è (e di nuovo, IMO, questo è buono).

    Cosa significa questo per C ++ e le implementazioni pratiche di C?

    Quindi, con la nefanda battuta dell’N685 … ‘messa da parte’ … siamo tornati ad assumere che i puntatori nella sequenza iniziale comune non sono speciali in termini di aliasing. Ancora. vale la pena confermare ciò che questo paragrafo in C ++ significa senza. Bene, il 2o thread GCC sopra link a un’altra gem:

    Difetto C ++ 1719 . Questa proposta ha raggiunto lo status di DRWP : “Un problema di DR la cui risoluzione si riflette nell’attuale documento di lavoro. Il documento di lavoro è una bozza per una versione futura dello standard” – cite . Questo è o post C ++ 14 o almeno dopo la bozza finale che ho qui (N3797) – e presenta un significativo, ea mio avviso illuminante, riscrittura del testo di questo paragrafo , come segue. Sono audace a quelli che considero i cambiamenti importanti e {questi commenti} sono miei:

    In un’unione con layout standard con un membro attivo {“active” indica un’istanza union , non solo tipo} (9.5 [class.union]) di struct di tipo T1 , è consentito leggere {formsrly “inspect”} un non- membro di dati statici m di un altro membro di unione di struct di tipo T2 fornito m fa parte della sequenza iniziale comune di T1 e T2 . [ Nota : la lettura di un object volatile tramite un glValue non volatile ha un comportamento indefinito (7.1.6.1 [dcl.type.cv]). -End note]

    Questo sembra chiarire il significato della vecchia formulazione: per me, si dice che qualsiasi specifica punibilità tra le struct membro del union con sequenze iniziali comuni deve essere fatta tramite un’istanza union parentale – piuttosto che essere basata sul tipo delle structs (ad es. i puntatori a loro passati a qualche funzione). Questa formulazione sembra escludere qualsiasi altra interpretazione, a la N685. C farei bene ad adottare questo, direi. Ehi, di cui, vedi sotto!

    Il risultato è che – come ben dimostrato da @ecatmur e dai biglietti GCC – questo lascia le struct membro del union per definizione in C ++, e praticamente in C, sobject alle stesse severe regole di aliasing di qualsiasi altro 2 puntatori ufficialmente non correlati. La garanzia esplicita di essere in grado di leggere la sequenza iniziale comune delle struct membri union inattivi è ora più chiaramente definita, escludendo la “visibilità” vaga e inimmaginabile tediosa per far rispettare la ” prova” di N685 per C. Con questa definizione, la principale i compilatori si sono comportati come previsto per C ++. Per quanto riguarda C?

    Possibile inversione di questa linea in C / chiarimento in C ++

    Vale anche la pena notare che Martin Sebor, membro del comitato di categoria C, sta cercando di sistemarlo in quel linguaggio raffinato:

    Martin Sebor 2015-04-27 14:57:16 UTC Se uno di voi può spiegare il problema con esso, sono disposto a scrivere un articolo e inviarlo a WG14 e chiedere di cambiare lo standard.

    Martin Sebor 2015-05-13 16:02:41 UTC Ho avuto la possibilità di discutere la questione con Clark Nelson la settimana scorsa. Clark ha lavorato sul miglioramento delle parti aliasing delle specifiche C in passato, ad esempio in N1520 ( http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm ). Ha convenuto che, come i problemi indicati nel N1520, questo è anche un problema eccezionale che varrebbe la pena di rivedere e correggere WG14 “.

    Potatoswatter conclude con entusiasmo:

    I comitati C e C ++ (attraverso Martin e Clark) cercheranno di trovare un consenso e di elaborare una formulazione in modo che lo standard possa finalmente dire cosa significa.

    Possiamo solo sperare!

    Ancora una volta, tutti gli ulteriori pensieri sono ben accetti.

    Sospetto che ciò significhi che l’accesso a queste parti comuni è consentito non solo attraverso il tipo di sindacato, ma al di fuori dell’unione. Cioè, supponiamo di avere questo:

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

    Supponiamo ora che in alcune funzioni abbiamo un puntatore struct s1 *p1 che sappiamo essere stato rimosso dal membro m1 di tale unione. Possiamo lanciarlo su un puntatore struct s2 * e accedere ancora ai membri che sono in comune con struct s1 . Ma da qualche parte nel campo di applicazione, una dichiarazione di union u deve essere visibile. E deve essere la dichiarazione completa, che informa il compilatore che i membri sono struct s1 e struct s2 .

    Il probabile intento è che se c’è un tale tipo di scope, allora il compilatore sa che struct s1 e struct s2 sono alias, e quindi si sospetta un accesso attraverso un struct s1 * pointer per accedere realmente a una struct s2 o viceversa.

    In assenza di qualsiasi tipo di unione visibile che unisce questi tipi in questo modo, non esiste tale conoscenza; può essere applicato un aliasing rigoroso.

    Poiché la dicitura è assente dal C ++, quindi per sfruttare la regola del “rilassamento iniziale dei membri comuni” in quella lingua, devi instradare gli accessi attraverso il tipo di unione, come è comunemente fatto in ogni caso:

     union u *ptr_any; // ... ptr_any->m1.common_initial_member = 42; fun(ptr_any->m2.common_initial_member); // pass 42 to fun