Una struct dice "tutti questi, insieme". Una union dice "esattamente uno di questi, alla volta". I membri sono sovrapposti allo stesso indirizzo, quindi la union è grande solo quanto il suo membro più grande e scrivere un membro distrugge gli altri.
Sembra una trappola, e senza controlli lo è. Ma è anche il modo in cui C esprime un valore che può essere di vari tipi, come un token che è un numero o una stringa, o un messaggio che è un clic o la pressione di un tasto, senza pagare per tutti contemporaneamente.
Dichiarare e usare una union
La sintassi ricalca esattamente quella di una struct; cambia solo la parola chiave.
Ai membri si accede con . (o con -> tramite un puntatore), esattamente come in una struct. La differenza è che solo il membro scritto più di recente contiene un valore sensato. Dopo v.f = 3.5f, leggere v.i non ti dà 3: ti dà l'intero che il pattern di bit di 3.5f si trova per caso a rappresentare.
Dimensione: vince il membro più grande
Confronta il layout in memoria di una struct e di una union con gli stessi membri:
Su una macchina tipica la struct occupa 24 byte (4 per l'int, 8 per il double, 1 per il char, più il padding) mentre la union ne occupa 8: la dimensione del suo double, arrotondata per l'allineamento. I tre indirizzi stampati sono identici, e questa è tutta la storia delle union in una riga di output.
Nota i cast a void * per %p. printf si aspetta esattamente quello per %p; passare un tipo di puntatore diverso è comportamento indefinito anche se di solito sembra funzionare. Vedi gli specificatori di formato.
Inizializzare una union
Un inizializzatore tra graffe senza designatore inizializza il primo membro:
La forma designata è quella da usare. {42} dipende in silenzio dall'ordine dei membri, quindi riordinare la dichiarazione in seguito cambia quale membro viene inizializzato: un bug davvero subdolo, perché nel codice non sembra cambiato niente.
Il vero problema: quale membro è attivo?
Una union non registra quale membro hai scritto per ultimo. Sono solo byte; l'informazione vive nella tua testa, ed è proprio lì che le informazioni si perdono.
Il numero stampato è grande e strano: il pattern di bit di 1.0f letto come int. Niente è andato in crash, nessun avviso, e il programma è silenziosamente sbagliato. La union ha fatto esattamente ciò che prometteva; l'errore è stato nostro, che abbiamo dimenticato quale membro era attivo.
La soluzione: una tagged union
La soluzione standard è abbinare la union a un enum che registra il membro attivo, e avvolgere entrambi in una struct. Questa combinazione si chiama tagged union (o union discriminata), ed è il modo in cui dovresti scrivere praticamente ogni union nel codice applicativo.
Ogni lettura ora passa per lo switch su kind, quindi è impossibile leggere un membro che non è mai stato scritto, purché ogni scrittura imposti anche il tag. Racchiudere le scritture in piccole funzioni costruttrici (value_from_int, value_from_string) è il modo abituale per rendere impossibile dimenticarlo.
Il risparmio di memoria è reale: ogni Value qui costa 24 byte di contenuto più il tag, invece di 4 + 4 + 24 per una struct che li contenesse tutti e tre. Con centomila elementi, fa la differenza.
Compilare con -Wall aggiunge una seconda rete di sicurezza: se in seguito aggiungi VAL_BOOL all'enum e dimentichi il relativo case, GCC ti avvisa del valore di enumerazione non gestito.
Union anonime
C11 permette di avere dentro una struct un membro union senza nome, i cui membri vengono promossi al namespace della struct esterna:
Poiché la union stessa non ha nome, scrivi s->circle.r invece di s->as.circle.r. Più breve da leggere, al prezzo di nascondere il fatto che c'è di mezzo una union, il che va bene quando il tag è lì accanto.
Dove le union si guadagnano davvero il posto
Quattro usi ricorrenti:
- Valori varianti. Interpreti, parser di JSON e di configurazione e code di messaggi trasportano tutti valori il cui tipo si decide a runtime. Una tagged union è la rappresentazione canonica.
- Record compatti in memoria. Quando una struct ha diversi campi mutuamente esclusivi e ne hai milioni, sovrapporli è un risparmio diretto.
- Layout di protocolli e hardware. Un pacchetto il cui contenuto dipende da un byte di intestazione si mappa in modo naturale su una tagged union, e lo stesso vale per i registri dei dispositivi.
- Ispezione dei byte. Sovrapporre un valore a un array di
unsigned charti permette di guardarne i singoli byte, per esempio per determinare l'endianness:
Questo è type punning: rileggere deliberatamente dei byte come un tipo diverso. Leggere tramite una union in questo modo è esplicitamente permesso in C come risultato definito dall'implementazione (a differenza del cast di puntatori tra tipi non correlati, che viola le regole di aliasing), e ispezionare i byte come unsigned char è sempre sicuro. Reinterpretare un int come float è un altro discorso: il risultato dipende interamente dalla rappresentazione della tua piattaforma, quindi tienilo fuori dal codice portabile.
Errori comuni
- Leggere un membro che non hai scritto. Il rischio principale. Usa un tag.
- Pensare che una union converta. Non lo fa.
u.i = 3; float f = u.f;reinterpreta i bit;int i = 3; float f = i;converte. Vedi il casting dei tipi. - Mettere un puntatore in una union e perderne traccia. Se un ramo contiene un
char *che hai allocato, sovrascrivere la union con un altro membro causa un memory leak: non resta niente che punti al buffer. Libera la memoria prima di cambiare ramo. - Aspettarsi che il compilatore controlli. Non lo farà. Le union sono una delle poche funzionalità di C in cui il linguaggio non offre alcun aiuto oltre a dimensione e allineamento; il tag è la tua unica protezione.
Domande frequenti
Cos'è una union in C?
Una union è un tipo i cui membri condividono tutti la stessa memoria. Scrivere un membro sovrascrive gli altri, quindi una union contiene esattamente uno dei suoi membri alla volta. Si dichiara come una struct ma con la parola chiave union: union Value { int i; float f; };.
Qual è la differenza tra union e struct in C?
Una struct dà a ogni membro il proprio spazio e li contiene tutti contemporaneamente, quindi la sua dimensione è almeno la somma dei membri. Una union sovrappone tutti i membri allo stesso indirizzo e ne contiene uno alla volta, quindi la sua dimensione è quella del membro più grande.
Qual è la dimensione di una union in C?
Abbastanza grande per il membro più grande, arrotondata per l'allineamento. Una union di un int (4 byte) e di un double (8 byte) occupa 8 byte, non 12. sizeof è il modo per verificarlo sulla tua piattaforma.
Cosa succede se leggi un membro di una union che non hai scritto?
Reinterpreti gli stessi byte come un tipo diverso. Scrivere u.i = 1 e poi leggere u.f non converte niente: legge il pattern di bit dell'intero come un float, e ottieni un numero senza senso. Nel C standard questo è nel migliore dei casi non specificato, ed è per questo che esiste il pattern tagged union: tenere accanto un tag che dice quale membro è attivo.