Accesso al membro del sindacato inattivo e comportamento indefinito?


129

Avevo l'impressione che l'accesso a un unionmembro diverso dall'ultimo set fosse UB, ma non riesco a trovare un riferimento solido (a parte le risposte che affermano che è UB ma senza alcun supporto dallo standard).

Quindi, è un comportamento indefinito?


3
C99 (e credo anche C ++ 11) consente esplicitamente la tipizzazione con i sindacati. Quindi penso che rientri nel comportamento "implementazione definita".
Mistico il

1
L'ho usato in diverse occasioni per convertire da int individuale a char. Quindi, sicuramente so che non è indefinito. L'ho usato sul compilatore Sun CC. Quindi, potrebbe essere ancora dipendente dal compilatore.
go4sri

42
@ go4sri: Chiaramente, non sai cosa significhi che il comportamento non è definito. Il fatto che in qualche caso sembrasse funzionare per te non contraddice la sua indefinibilità.
Benjamin Lindley


4
@Mysticial, il post sul blog a cui ti colleghi riguarda in modo molto specifico C99; questa domanda è taggata solo per C ++.
davmac,

Risposte:


131

La confusione è che C consente esplicitamente la punzonatura di tipo attraverso un sindacato, mentre C ++ () non ha tale permesso.

6.5.2.3 Struttura e membri del sindacato

95) Se il membro utilizzato per leggere il contenuto di un oggetto unione non è lo stesso dell'ultimo membro utilizzato per memorizzare un valore nell'oggetto, la parte appropriata della rappresentazione dell'oggetto del valore viene reinterpretata come rappresentazione dell'oggetto nel nuovo digitare come descritto in 6.2.6 (un processo talvolta chiamato '' punzonatura ''). Questa potrebbe essere una rappresentazione trappola.

La situazione con C ++:

9.5 Unions [class.union]

In un'unione, al massimo uno dei membri di dati non statici può essere attivo in qualsiasi momento, ovvero il valore di al massimo uno dei membri di dati non statici può essere archiviato in un'unione in qualsiasi momento.

C ++ in seguito ha un linguaggio che consente l'uso di sindacati contenenti structs con sequenze iniziali comuni; ciò non consente tuttavia la punitura di tipo.

Per determinare se la punzonatura dei tipi di unione è consentita in C ++, dobbiamo cercare ulteriormente. Richiama questo è un riferimento normativo per C ++ 11 (e C99 ha un linguaggio simile a C11 che consente la punitura dei tipi di unione):

3.9 Tipi [tipi di base]

4 - La rappresentazione dell'oggetto di un oggetto di tipo T è la sequenza di N oggetti char senza segno ripresi dall'oggetto di tipo T, dove N è uguale a sizeof (T). La rappresentazione di valore di un oggetto è l'insieme di bit che contengono il valore di tipo T. Per tipi banalmente copiabili, la rappresentazione di valore è un insieme di bit nella rappresentazione di oggetto che determina un valore, che è un elemento discreto di un'implementazione- set di valori definito. 42
42) L'intento è che il modello di memoria di C ++ sia compatibile con quello del linguaggio di programmazione C. ISO / IEC 9899

Diventa particolarmente interessante quando leggiamo

3.8 Durata dell'oggetto [basic.life]

La durata di un oggetto di tipo T inizia quando: - si ottiene una memoria con il corretto allineamento e dimensione per il tipo T e - se l'oggetto ha un'inizializzazione non banale, la sua inizializzazione è completa.

Quindi per un tipo primitivo (che ipso facto ha una banale inizializzazione) contenuto in un'unione, la durata dell'oggetto comprende almeno la durata dell'unione stessa. Questo ci consente di invocare

3.9.2 Tipi di composti [basic.compound]

Se un oggetto di tipo T si trova su un indirizzo A, un puntatore di tipo cv T * il cui valore è l'indirizzo A punta a quell'oggetto, indipendentemente da come è stato ottenuto il valore.

Supponendo che l'operazione a cui siamo interessati sia la punzonatura del tipo, cioè prendendo il valore di un membro sindacale non attivo, e dato quanto sopra che abbiamo un riferimento valido all'oggetto a cui fa riferimento quel membro, tale operazione è lvalue-to -valutazione conversione:

4.1 Conversione da valore in valore [conv.lval]

Un valore nominale di un tipo non funzionale, non array Tpuò essere convertito in un valore. Se Tè un tipo incompleto, un programma che richiede questa conversione è mal formato. Se l'oggetto a cui si riferisce il valore gluteo non è un oggetto di tipo Te non è un oggetto di un tipo derivato To se l'oggetto non è inizializzato, un programma che richiede questa conversione ha un comportamento indefinito.

La domanda quindi è se un oggetto che è un membro del sindacato non attivo viene inizializzato dalla memoria sul membro del sindacato attivo. Per quanto posso dire, questo non è il caso e quindi anche se:

  • un'unione viene copiata nella charmemoria dell'array e viceversa (3.9: 2), oppure
  • un'unione viene copiata per byte in un'altra unione dello stesso tipo (3.9: 3), oppure
  • a un sindacato si accede attraverso i confini del linguaggio da un elemento del programma conforme a ISO / IEC 9899 (per quanto definito) (3.9: 4 nota 42), quindi

l'accesso a un'unione da parte di un membro non attivo è definito ed è definito per seguire l'oggetto e la rappresentazione del valore, l'accesso senza una delle interposizioni sopra è un comportamento indefinito. Ciò ha implicazioni per le ottimizzazioni che possono essere eseguite su un tale programma, poiché l'implementazione può ovviamente presumere che non si verifichino comportamenti indefiniti.

Cioè, anche se possiamo legittimamente formare un valore per un membro del sindacato non attivo (motivo per cui l'assegnazione a un membro non attivo senza costruzione è ok) è considerato non inizializzato.


5
3.8 / 1 dice che la durata di un oggetto termina quando la sua memoria viene riutilizzata. Ciò indica che un membro non attivo della vita di un sindacato è terminato perché il suo archivio è stato riutilizzato per il membro attivo. Ciò significherebbe che sei limitato nel modo in cui usi il membro (3.8 / 6).
bames53,

2
In base a tale interpretazione, ogni bit di memoria contiene simultaneamente oggetti di tutti i tipi che sono banalmente inizializzabili e hanno un allineamento appropriato ... Quindi la durata di qualsiasi tipo non banalmente inizializzabile termina immediatamente quando la sua memorizzazione viene riutilizzata per tutti questi altri tipi ( e non ricominciare perché non sono banalmente inizializzabili)?
bames53,

3
Il testo 4.1 è completamente e completamente rotto e da allora è stato riscritto. Non consentiva ogni sorta di cose perfettamente valide: non consentiva memcpyimplementazioni personalizzate (accedendo agli oggetti usando i unsigned charvalori), non consentiva gli accessi a *pafter int *p = 0; const int *const *pp = &p;(anche se la conversione implicita da int**a const int*const*è valida), non consentiva nemmeno l'accesso cdopo struct S s; const S &c = s;. Numero CWG 616 . La nuova formulazione lo consente? C'è anche [basic.lval].

2
@Omnifario: questo avrebbe senso, anche se avrebbe anche bisogno di chiarire (e lo standard C deve anche chiarire, tra l'altro) cosa &intende l'operatore unario quando applicato a un membro del sindacato. Penserei che il puntatore risultante dovrebbe essere utilizzabile per accedere al membro almeno fino alla prossima volta il prossimo uso diretto o indiretto di qualsiasi altro valore del membro, ma in gcc il puntatore non è utilizzabile nemmeno così a lungo, il che solleva una domanda su cosa l' &operatore dovrebbe significare.
supercat

4
Una domanda riguardante "Ricorda che c99 è un riferimento normativo per C ++ 11" Non è rilevante solo quando lo standard c ++ si riferisce esplicitamente allo standard C (ad es. Per le funzioni della libreria c)?
MikeMB,

28

Lo standard C ++ 11 lo dice in questo modo

9.5 I sindacati

In un'unione, al massimo uno dei membri di dati non statici può essere attivo in qualsiasi momento, ovvero il valore di al massimo uno dei membri di dati non statici può essere archiviato in un'unione in qualsiasi momento.

Se viene memorizzato solo un valore, come puoi leggerne un altro? Semplicemente non c'è.


La documentazione di gcc lo elenca in Comportamento definito dall'implementazione

  • È possibile accedere a un membro di un oggetto unione utilizzando un membro di tipo diverso (C90 6.3.2.3).

I byte rilevanti della rappresentazione dell'oggetto vengono trattati come un oggetto del tipo utilizzato per l'accesso. Vedi Punzonatura. Questa potrebbe essere una rappresentazione trappola.

indicando che ciò non è richiesto dalla norma C.


05-01-2016: Attraverso i commenti sono stato collegato al Rapporto sui difetti C99 n. 283 che aggiunge un testo simile come nota a piè di pagina al documento standard C:

78a) Se il membro utilizzato per accedere al contenuto di un oggetto unione non è lo stesso dell'ultimo membro utilizzato per memorizzare un valore nell'oggetto, la parte appropriata della rappresentazione oggetto del valore viene reinterpretata come rappresentazione oggetto nel nuovo digitare come descritto in 6.2.6 (un processo talvolta chiamato "tipo punning"). Questa potrebbe essere una rappresentazione trappola.

Non sono sicuro che chiarisca molto però, considerando che una nota a piè di pagina non è normativa per lo standard.


10
@LuchianGrigore: UB non è ciò che lo standard dice UB, invece è ciò che lo standard non descrive come dovrebbe funzionare. Questo è esattamente questo caso. Lo standard descrive cosa succede? Dice che è definita l'implementazione? No e no Quindi è UB. Inoltre, per quanto riguarda l'argomento "i membri condividono lo stesso indirizzo di memoria", dovrai fare riferimento alle regole di aliasing, che ti porteranno di nuovo su UB.
Yakov Galka

5
@Luchian: È abbastanza chiaro cosa significa attivo, "cioè, il valore di al massimo uno dei membri di dati non statici può essere archiviato in un sindacato in qualsiasi momento".
Benjamin Lindley

5
@LuchianGrigore: Sì, ci sono. Esistono infiniti casi che lo standard non (e non può) affrontare. (C ++ è una VM completa di Turing, quindi è incompleta.) E allora? Spiega cosa significa "attivo", fare riferimento alla citazione sopra, dopo "cioè".
Yakov Galka

8
@LuchianGrigore: l'omissione della definizione esplicita di comportamento è anche un comportamento indefinito non considerato, secondo la sezione delle definizioni.
jxh

5
@Claudiu Questo è UB per un motivo diverso: viola un aliasing rigoroso.
Mistico il

18

Penso che il più vicino a cui lo standard viene a dire che il suo comportamento indefinito sia il luogo in cui definisce il comportamento di un'unione contenente una sequenza iniziale comune (C99, §6.5.2.3 / 5):

Viene fornita una garanzia speciale per semplificare l'uso dei sindacati: se un'unione contiene più strutture che condividono una sequenza iniziale comune (vedi sotto) e se l'oggetto unione contiene attualmente una di queste strutture, è consentito ispezionare il comune parte iniziale di uno di essi ovunque sia visibile una dichiarazione del tipo completo di unione. Due strutture condividono una sequenza iniziale comune se i membri corrispondenti hanno tipi compatibili (e, per i campi di bit, le stesse larghezze) per una sequenza di uno o più membri iniziali.

C ++ 11 fornisce requisiti / autorizzazioni simili al § 9.2 / 19:

Se un'unione di layout standard contiene due o più strutture di layout standard che condividono una sequenza iniziale comune e se l'oggetto unione di layout standard contiene attualmente una di queste strutture di layout standard, è consentito ispezionare la parte iniziale comune di qualsiasi di loro. Due strutture di layout standard condividono una sequenza iniziale comune se i membri corrispondenti hanno tipi compatibili con il layout e nessuno dei due membri è un campo di bit o entrambi sono campi di bit con la stessa larghezza per una sequenza di uno o più membri iniziali.

Sebbene nessuno dei due lo affermi direttamente, entrambi hanno una forte implicazione che "ispezionare" (leggere) un membro è "permesso" solo se 1) è (parte di) il membro scritto più di recente, o 2) fa parte di una iniziale comune sequenza.

Non è un'affermazione diretta che fare diversamente sia un comportamento indefinito, ma è il più vicino di cui sono a conoscenza.


Per renderlo completo, devi sapere quali "tipi compatibili con il layout" sono per C ++ o "tipi compatibili" per C.
Michael Anderson,

2
@MichaelAnderson: Sì e no. Devi avere a che fare con quelli quando / se vuoi essere certo che qualcosa rientri in questa eccezione - ma la vera domanda qui è se qualcosa che chiaramente non rientra nell'eccezione dà davvero UB. Penso che sia abbastanza implicito qui per chiarire l'intento, ma non penso che sia mai stato dichiarato direttamente.
Jerry Coffin,

Questa cosa della "sequenza iniziale comune" potrebbe aver salvato 2 o 3 dei miei progetti dal Cestino di riscrittura. Sono stato livido quando ho letto per la prima volta che la maggior parte degli usi punitivi di unionessere indefinito, dal momento che mi era stato dato l'impressione da un blog particolare che questo fosse OK, e ho costruito diverse grandi strutture e progetti attorno ad esso. Ora penso che potrei essere OK dopo tutto, dato che i miei unioncontengono classi con gli stessi tipi nella parte anteriore
underscore_d

@JerryCoffin, penso che mi stia suggerendo la stessa domanda per me: cosa succede se il nostro unioncontiene ad esempio a uint8_te a class Something { uint8_t myByte; [...] };- Suppongo che questa condizione si applicherebbe anche qui, ma è formulata in modo molto deliberato per consentire solo structs. Fortunatamente sto già usando quelli al posto dei primitivi grezzi: O
underscore_d

@underscore_d: lo standard C almeno copre una sorta di domanda: "Un puntatore a un oggetto struttura, opportunamente convertito, punta al suo membro iniziale (o se quel membro è un campo di bit, quindi all'unità in cui risiede) , e viceversa."
Jerry Coffin,

12

Qualcosa che non è ancora menzionato dalle risposte disponibili è la nota 37 nel paragrafo 21 della sezione 6.2.5:

Si noti che il tipo aggregato non include il tipo di unione poiché un oggetto con tipo di unione può contenere solo un membro alla volta.

Questo requisito sembra implicare chiaramente che non devi scrivere in un membro e leggere in un altro. In questo caso potrebbe essere un comportamento indefinito per mancanza di specifiche.


Molte implementazioni documentano i formati di archiviazione e le regole di layout. Una specifica del genere implicherebbe in molti casi quale sarebbe l'effetto della lettura della memoria di un tipo e della scrittura come un altro in assenza di regole secondo le quali i compilatori non devono effettivamente utilizzare il loro formato di memoria definito, tranne quando le cose vengono lette e scritte usando i puntatori di un tipo di carattere.
Supercat,

-3

Spiego bene questo con un esempio.
supponiamo di avere la seguente unione:

union A{
   int x;
   short y[2];
};

Suppongo che sizeof(int)dia 4, e che sizeof(short)dia 2.
quando scrivi union A a = {10}bene crea una nuova var di tipo A inserendola nel valore 10.

la tua memoria dovrebbe apparire così: (ricorda che tutti i membri del sindacato hanno la stessa posizione)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

come puoi vedere, il valore di ax è 10, il valore di ay 1 è 10 e il valore di ay [0] è 0.

ora, cosa succede se lo faccio?

a.y[0] = 37;

la nostra memoria sarà simile a questa:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

questo trasformerà il valore di ax su 2424842 (in decimale).

ora, se la tua unione ha un float, o raddoppia, la tua mappa di memoria potrebbe essere più un casino, a causa del modo in cui memorizzi i numeri esatti. maggiori informazioni qui .


18
:) Questo non è quello che ho chiesto. So cosa succede internamente. So che funziona. Ho chiesto se è nello standard.
Luchian Grigore,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.