Scopo dei sindacati in C e C ++


254

Ho usato i sindacati prima comodamente; oggi sono stato allarmato quando ho letto questo post e ho scoperto che questo codice

union ARGB
{
    uint32_t colour;

    struct componentsTag
    {
        uint8_t b;
        uint8_t g;
        uint8_t r;
        uint8_t a;
    } components;

} pixel;

pixel.colour = 0xff040201;  // ARGB::colour is the active member from now on

// somewhere down the line, without any edit to pixel

if(pixel.components.a)      // accessing the non-active member ARGB::components

è in realtà un comportamento indefinito Vale a dire leggere da un membro del sindacato diverso da quello scritto di recente porta a un comportamento indefinito. Se questo non è l'uso previsto dei sindacati, che cos'è? Qualcuno può spiegarlo in modo elaborato?

Aggiornare:

Volevo chiarire alcune cose con il senno di poi.

  • La risposta alla domanda non è la stessa per C e C ++; il mio io più giovane ignorante lo ha etichettato sia come C che C ++.
  • Dopo aver passato in rassegna lo standard di C ++ 11, non ho potuto dire in modo definitivo che invoca / ispeziona un membro sindacale non attivo non definito / non specificato / definito dall'implementazione. Tutto quello che ho trovato è stato §9.5 / 1:

    Se un'unione di layout standard contiene diverse strutture di layout standard che condividono una sequenza iniziale comune e se un oggetto di questo tipo di unione di layout standard contiene una delle strutture di layout standard, è consentito ispezionare la sequenza iniziale comune di qualsiasi dei membri della struttura a layout standard. §9.2 / 19: 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 una o più iniziali membri.

  • Mentre sei in C, ( C99 TC3 - DR 283 in poi) è legale farlo ( grazie a Pascal Cuoq per averlo menzionato ). Tuttavia, tentare di farlo può comunque portare a comportamenti indefiniti , se il valore letto risulta non valido (la cosiddetta "rappresentazione trap") per il tipo che viene letto. Altrimenti, il valore letto è definito dall'implementazione.
  • C89 / 90 lo ha definito sotto un comportamento non specificato (Allegato J) e il libro di K&R afferma che la sua implementazione è stata definita. Citazione da K&R:

    Questo è lo scopo di un'unione - una singola variabile che può legittimamente contenere uno di diversi tipi. [...] purché l'uso sia coerente: il tipo recuperato deve essere il tipo memorizzato più di recente. È responsabilità del programmatore tenere traccia di quale tipo è attualmente memorizzato in un sindacato; i risultati dipendono dall'implementazione se qualcosa viene archiviato come un tipo ed estratto come un altro.

  • Estratto dal TC ++ PL di Stroustrup (il mio accento è mio)

    L'uso dei sindacati può essere essenziale per la compatibilità dei dati [...] talvolta utilizzati in modo improprio per la "conversione del tipo ".

Soprattutto, questa domanda (il cui titolo rimane invariato dalla mia domanda) è stata posta con l'intenzione di comprendere lo scopo dei sindacati E non su ciò che lo standard consente Ad es. L'uso dell'ereditarietà per il riutilizzo del codice è, ovviamente, consentito dallo standard C ++, ma non era lo scopo o l'intenzione originale di introdurre l'ereditarietà come funzionalità del linguaggio C ++ . Questo è il motivo per cui la risposta di Andrey continua a rimanere quella accettata.


11
In poche parole, i compilatori possono inserire imbottiture tra gli elementi di una struttura. Pertanto, b, g, r,e apotrebbe non essere contiguo, e quindi non corrispondere al layout di a uint32_t. Ciò si aggiunge alle questioni di Endianess che altri hanno sottolineato.
Thomas Matthews,

8
Questo è esattamente il motivo per cui non dovresti etichettare le domande C e C ++. Le risposte sono diverse, ma dal momento che i rispondenti non dicono nemmeno per quale tag stanno rispondendo (lo sanno nemmeno?), Ottieni spazzatura.
Pascal Cuoq,

5
@downvoter Grazie per non aver spiegato, capisco che vuoi che capisca magicamente la tua lamentela e non la ripeta in futuro: P
legends2k

1
Per quanto riguarda l'intenzione originaria di avere l' unione , tenere presente che i C standard post-datano i sindacati C di diversi anni. Una rapida occhiata a Unix V7 mostra alcune conversioni di tipo tramite i sindacati.
ninjalj,

3
scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1...veramente? si cita una nota di eccezione , non il punto principale all'inizio del paragrafo : "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 i membri di dati non statici possono essere archiviati in un sindacato in qualsiasi momento. " - e fino a p4: "In generale, si devono usare chiamate esplicite al distruttore e posizionare nuovi operatori per cambiare il membro attivo di un sindacato "
underscore_d

Risposte:


407

Lo scopo dei sindacati è piuttosto ovvio, ma per qualche ragione la gente lo manca abbastanza spesso.

Lo scopo dell'unione è quello di risparmiare memoria utilizzando la stessa area di memoria per archiviare oggetti diversi in momenti diversi. Questo è tutto.

È come una stanza in un hotel. Diverse persone vivono in esso per periodi di tempo non sovrapposti. Queste persone non si incontrano mai e generalmente non sanno nulla l'una dell'altra. Gestendo correttamente la condivisione del tempo delle stanze (ovvero assicurandosi che persone diverse non vengano assegnate contemporaneamente a una stanza), un hotel relativamente piccolo può fornire alloggi a un numero relativamente elevato di persone, che è ciò che gli hotel sono per.

Questo è esattamente ciò che fa l'unione. Se sai che diversi oggetti nel tuo programma contengono valori con durata dei valori non sovrapponibili, puoi "unire" questi oggetti in un'unione e quindi risparmiare memoria. Proprio come una camera d'albergo ha al massimo un inquilino "attivo" in ogni momento del tempo, un sindacato ha al massimo un membro "attivo" in ogni momento del programma. È possibile leggere solo il membro "attivo". Scrivendo in un altro membro si passa lo stato "attivo" a quell'altro membro.

Per qualche ragione, questo scopo originale del sindacato è stato "ignorato" con qualcosa di completamente diverso: scrivere un membro di un sindacato e poi ispezionarlo attraverso un altro membro. Questo tipo di reinterpretazione della memoria (alias "type punning") non è un uso valido dei sindacati. Generalmente porta a comportamenti indefiniti che viene descritto come producendo comportamenti definiti dall'implementazione in C89 / 90.

EDIT: L' uso dei sindacati ai fini della punzonatura di tipo (cioè scrivere un membro e poi leggerne un altro) è stata data una definizione più dettagliata in una delle rettifiche tecniche allo standard C99 (vedi DR # 257 e DR # 283 ). Tuttavia, tieni presente che formalmente questo non ti protegge dall'incontrare un comportamento indefinito tentando di leggere una rappresentazione trappola.


37
+1 per essere elaborato, dare un semplice esempio pratico e dire dell'eredità dei sindacati!
legends2k,

6
Il problema che ho con questa risposta è che la maggior parte dei sistemi operativi che ho visto hanno file di intestazione che fanno esattamente questa cosa. Ad esempio, l'ho visto nelle vecchie versioni (pre-64-bit) di <time.h>Windows e Unix. Respingerlo come "non valido" e "non definito" non è davvero sufficiente se sarò chiamato a capire il codice che funziona in questo modo esatto.
TED

31
@AndreyT “Non è mai stato legale utilizzare i sindacati per la punzonatura di tipo fino a poco tempo fa”: il 2004 non è “molto recente”, soprattutto considerando che è solo il C99 inizialmente formulato in modo goffo, che sembra rendere la punzonatura di tipo attraverso i sindacati non definita. In realtà, la punizione dei tipi, sebbene i sindacati sia legale in C89, legale in C11, e legale in C99 da sempre, anche se ci sono voluti fino al 2004 per il comitato per correggere una formulazione errata e il successivo rilascio di TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq

6
@ legends2k Il linguaggio di programmazione è definito dallo standard. Il Corrigendum tecnico 3 dello standard C99 consente esplicitamente la punzonatura di tipo nella sua nota 82, che vi invito a leggere da soli. Questa non è la TV in cui le rock star vengono intervistate ed esprimono le loro opinioni sul cambiamento climatico. L'opinione di Stroustrup non ha influenza su ciò che dice lo standard C.
Pascal Cuoq,

6
@ legends2k " So che l'opinione di ogni individuo non ha importanza e solo lo standard fa " L'opinione degli autori di compilatori è molto più importante della "specifica" del linguaggio (estremamente scadente).
curiousguy,

38

È possibile utilizzare i sindacati per creare strutture come la seguente, che contiene un campo che ci dice quale componente dell'unione viene effettivamente utilizzato:

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;

Sono totalmente d'accordo, senza entrare nel caos del comportamento indefinito, forse questo è il comportamento previsto migliore dei sindacati a cui riesco a pensare; ma non è spreco di spazio quando sto solo usando, diciamo into char*per 10 oggetti di oggetto []; in tal caso, posso effettivamente dichiarare strutture separate per ciascun tipo di dati anziché VAROBJECT? Non ridurrebbe il disordine e userebbe meno spazio?
legends2k,

3
legende: in alcuni casi, semplicemente non puoi farlo. Usi qualcosa come VAROBJECT in C negli stessi casi quando usi Object in Java.
Erich Kitzmueller,

La struttura dei dati dei sindacati con tag sembra essere un solo uso legittimo dei sindacati, come spiegato.
legends2k

Fornisci anche un esempio di come utilizzare i valori.
Ciro Santilli 4 冠状 病 六四 事件 法轮功

1
@CiroSantilli 新疆 改造 中心 六四 事件 法轮功 Potrebbe essere utile una parte di un esempio di C ++ Primer . wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick

34

Il comportamento non è definito dal punto di vista linguistico. Considera che piattaforme diverse possono avere vincoli diversi nell'allineamento e nell'endianità della memoria. Il codice in una macchina big endian contro una macchina little endian aggiornerà i valori nella struttura in modo diverso. La correzione del comportamento nella lingua richiederebbe a tutte le implementazioni di utilizzare lo stesso endianness (e vincoli di allineamento della memoria ...) limitandone l'uso.

Se stai usando C ++ (stai usando due tag) e ti interessa davvero la portabilità, allora puoi semplicemente usare la struttura e fornire un setter che prende uint32_te imposta i campi in modo appropriato attraverso le operazioni di maschera di bit. Lo stesso può essere fatto in C con una funzione.

Modifica : mi aspettavo che AProgrammer scrivesse una risposta per votare e chiudesse questa. Come hanno sottolineato alcuni commenti, l'endianità viene trattata in altre parti dello standard lasciando che ogni implementazione decida cosa fare e l'allineamento e il riempimento possono anche essere gestiti in modo diverso. Ora, le rigide regole di alias a cui AProgrammer si riferisce implicitamente sono un punto importante qui. Al compilatore è consentito fare ipotesi sulla modifica (o mancanza di modifica) delle variabili. Nel caso dell'unione, il compilatore potrebbe riordinare le istruzioni e spostare la lettura di ciascun componente di colore sulla scrittura nella variabile colore.


+1 per la risposta chiara e semplice! Sono d'accordo, per portabilità, il metodo che hai dato nel secondo paragrafo è valido; ma posso usare il modo in cui ho posto la domanda, se il mio codice è legato a una singola architettura (pagando il prezzo della protezione), dal momento che salva 4 byte per ogni valore di pixel e un po 'di tempo risparmiato nell'esecuzione di quella funzione ?
legends2k,

Il problema endian non obbliga lo standard a dichiararlo come comportamento indefinito: reinterpret_cast ha esattamente gli stessi problemi endian, ma ha un comportamento definito dall'implementazione.
Joe G

1
@ legends2k, il problema è che l'ottimizzatore può presumere che un uint32_t non venga modificato scrivendo in un uint8_t e quindi ottieni il valore sbagliato quando l'uso ottimizzato di tale presupposto ... @Joe, il comportamento indefinito appare non appena accedi al puntatore (lo so, ci sono alcune eccezioni).
Programmatore il

1
@ legends2k / AProgrammer: il risultato di un reinterpret_cast è definito dall'implementazione. L'uso del puntatore restituito non comporta un comportamento indefinito, ma solo un comportamento definito dall'implementazione. In altre parole, il comportamento deve essere coerente e definito, ma non è portatile.
Joe G

1
@ legends2k: qualsiasi ottimizzatore decente riconoscerà le operazioni bit a bit che selezionano un intero byte e generano codice per leggere / scrivere il byte, come l'unione ma ben definito (e portatile). ad esempio uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | r; }
Ben Voigt,

22

L' uso più comune di cui unionmi imbatto regolarmente è l' aliasing .

Considera quanto segue:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

Cosa fa questo? Permette l'accesso pulito e ordinato dei Vector3f vec;membri di uno con uno dei due nomi:

vec.x=vec.y=vec.z=1.f ;

o tramite accesso intero nell'array

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

In alcuni casi, l'accesso per nome è la cosa più chiara che puoi fare. In altri casi, specialmente quando l'asse viene scelto a livello di codice, la cosa più semplice da fare è accedere all'asse tramite indice numerico: 0 per x, 1 per ye 2 per z.


3
Questo è anche chiamato type-punningche è anche menzionato nella domanda. Anche l'esempio nella domanda mostra un esempio simile.
legends2k

4
Non è una battuta di tipo. Nel mio esempio i tipi corrispondono , quindi non esiste un "gioco di parole", è semplicemente un aliasing.
bobobobo,

3
Sì, ma comunque, da un punto di vista assoluto dello standard linguistico, i membri scritti e letti sono diversi, il che non è definito come indicato nella domanda.
legends2k,

3
Spero che uno standard futuro risolva questo caso particolare in base alla regola della "susseguenza iniziale comune". Tuttavia, le matrici non partecipano a tale regola in base alla formulazione attuale.
Ben Voigt,

3
@curiousguy: chiaramente non è richiesto che i membri della struttura vengano posizionati senza imbottitura arbitraria. Se il test del codice per il posizionamento del membro della struttura o la dimensione della struttura, il codice dovrebbe funzionare se gli accessi vengono eseguiti direttamente attraverso l'unione, ma una lettura rigorosa dello standard indicherebbe che la presa dell'indirizzo di un sindacato o membro della struttura produce un puntatore che non può essere utilizzato come puntatore del proprio tipo, ma deve prima essere riconvertito in un puntatore nel tipo allegato o in un tipo di carattere. Qualsiasi compilatore utilizzabile in remoto estenderà il linguaggio facendo funzionare più cose di ...
supercat,

10

Come dici tu, questo è un comportamento strettamente indefinito, anche se "funzionerà" su molte piattaforme. La vera ragione per usare i sindacati è creare record di varianti.

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

Naturalmente, hai anche bisogno di una sorta di discriminatore per dire cosa contiene effettivamente la variante. E nota che in C ++ i sindacati non sono molto utili perché possono contenere solo tipi di POD - effettivamente quelli senza costruttori e distruttori.


L'hai usato così (come nella domanda) ?? :)
legends2k,

È un po 'pedante, ma non accetto del tutto "record di varianti". Cioè, sono sicuro che fossero in mente, ma se fossero una priorità perché non fornirli? "Fornire il blocco predefinito perché potrebbe essere utile costruire anche altre cose" sembra intuitivamente più probabile. Soprattutto data almeno un'altra applicazione che probabilmente era in mente - i registri I / O mappati in memoria, in cui i registri di input e output (mentre sovrapposti) sono entità distinte con i loro nomi, tipi ecc.
Steve314

@ Stev314 Se quello fosse l'uso che avevano in mente, avrebbero potuto renderlo non un comportamento indefinito.

@Neil: +1 per il primo a dire dell'uso effettivo senza colpire un comportamento indefinito. Immagino che avrebbero potuto rendere l'implementazione definita come altre operazioni di punzonatura di tipo (reinterpret_cast, ecc.). Ma come ti ho chiesto, l'hai usato per la punzonatura?
legends2k,

@Neil - l'esempio di registro mappato in memoria non è indefinito, il solito endian / etc a parte e viene dato un flag "volatile". Scrivere su un indirizzo in questo modello non fa riferimento allo stesso registro della lettura dello stesso indirizzo. Pertanto non vi è alcun problema di "cosa stai leggendo" in quanto non stai leggendo - qualunque output tu abbia scritto a quell'indirizzo, quando leggi stai solo leggendo un input indipendente. L'unico problema è assicurarsi di leggere il lato di input dell'unione e scrivere il lato di output. Era comune nelle cose incorporate - probabilmente lo è ancora.
Steve314,

8

In C è stato un bel modo di implementare qualcosa come una variante.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

In tempi di poca memoria questa struttura utilizza meno memoria di una struttura che ha tutto il membro.

A proposito C fornisce

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

per accedere ai valori dei bit.


Sebbene entrambi i tuoi esempi siano perfettamente definiti nello standard; ma, ehi, usando i campi bit è sicuramente sparato un codice non portabile, non è vero?
legends2k,

No non lo è. Per quanto ne so è ampiamente supportato.
Totonga,

1
Il supporto del compilatore non si traduce in portatile. Il libro C : C (quindi C ++) non fornisce alcuna garanzia per l'ordinamento dei campi all'interno delle parole automatiche, quindi se li usi per quest'ultima ragione, il tuo programma non sarà solo non portatile, ma dipenderà anche dal compilatore.
legends2k

5

Sebbene questo sia un comportamento strettamente indefinito, in pratica funzionerà praticamente con qualsiasi compilatore. È un paradigma così ampiamente usato che qualsiasi compilatore che si rispetti dovrà fare "la cosa giusta" in casi come questo. È certamente da preferire al tipo di punzonatura, che potrebbe generare codice rotto con alcuni compilatori.


2
Non c'è un problema endian? Una soluzione relativamente semplice rispetto a "non definita", ma in tal caso vale la pena tener conto di alcuni progetti.
Steve314,

5

In C ++, Boost Variant implementa una versione sicura dell'unione, progettata per prevenire il più possibile comportamenti indefiniti.

Le sue prestazioni sono identiche al enum + unioncostrutto (allocato anche nello stack, ecc.) Ma utilizza un elenco di modelli di tipi anziché enum:)


5

Il comportamento potrebbe non essere definito, ma ciò significa che non esiste uno "standard". Tutti i compilatori decenti offrono #pragmas per controllare l'imballaggio e l'allineamento, ma possono avere impostazioni predefinite diverse. Le impostazioni predefinite cambieranno anche in base alle impostazioni di ottimizzazione utilizzate.

Inoltre, i sindacati non sono solo per risparmiare spazio. Possono aiutare i compilatori moderni con la punzonatura di tipo. Se reinterpret_cast<>tutto il compilatore non può fare ipotesi su quello che stai facendo. Potrebbe essere necessario eliminare ciò che sa sul tuo tipo e ricominciare (forzando una riscrittura in memoria, che è molto inefficiente in questi giorni rispetto alla velocità di clock della CPU).


4

Tecnicamente non è definito, ma in realtà la maggior parte (tutti?) I compilatori lo trattano esattamente come usando un reinterpret_castda un tipo all'altro, il cui risultato è l'implementazione definita. Non perderei sonno sul tuo codice attuale.


" un reinterpret_cast da un tipo all'altro, il cui risultato è l'implementazione definita. " No, non lo è. Le implementazioni non devono definirlo e la maggior parte non lo definisce. Inoltre, quale sarebbe il comportamento definito dall'implementazione consentita di trasmettere un valore casuale a un puntatore?
curiousguy,

4

Per un altro esempio dell'uso effettivo dei sindacati, il framework CORBA serializza gli oggetti usando l'approccio sindacato con tag. Tutte le classi definite dall'utente sono membri di un'unione (enorme) e un identificatore intero indica al demarshaller come interpretare l'unione.


4

Altri hanno menzionato le differenze di architettura (little - big endian).

Ho letto il problema che, poiché la memoria per le variabili è condivisa, quindi scrivendo su una, le altre cambiano e, a seconda del loro tipo, il valore potrebbe essere privo di significato.

per esempio. union {float f; int i; } X;

Scrivere su xi non avrebbe senso se poi leggessi da xf - a meno che non fosse quello che intendevi per guardare il segno, l'esponente o le componenti della mantissa del galleggiante.

Penso che ci sia anche un problema di allineamento: se alcune variabili devono essere allineate a parole, potresti non ottenere il risultato atteso.

per esempio. unione {char c [4]; int i; } X;

Se, ipoteticamente, su una macchina un carattere dovesse essere allineato a parole, allora c [0] e c [1] condivideranno la memoria con i ma non c [2] e c [3].


Un byte che deve essere allineato a parole? Non ha senso. Un byte non ha requisiti di allineamento, per definizione.
curiousguy,

Sì, probabilmente avrei dovuto usare un esempio migliore. Grazie.
philcolbourn,

@curiousguy: ci sono molti casi in cui si potrebbe desiderare che le matrici di byte siano allineate a parole. Se uno ha molti array, ad esempio 1024 byte, e spesso desidera copiarne uno l'uno con l'altro, il fatto che siano allineati a una parola può su molti sistemi raddoppiare la velocità di uno memcpy()da uno all'altro. Alcuni sistemi potrebbero allineare speculativamente le char[]allocazioni che si verificano al di fuori di strutture / sindacati per questo e altri motivi. Nell'esempio esistente, il presupposto che isi sovrapporrà tutto per gli elementi di c[]non è portatile, ma è perché non esiste alcuna garanzia sizeof(int)==4.
supercat

4

Nel linguaggio C come documentato nel 1974, tutti i membri della struttura condividevano uno spazio dei nomi comune e il significato di "ptr-> member" era definito come l'aggiunta dello spostamento del membro a "ptr" e l'accesso all'indirizzo risultante usando il tipo di membro. Questo design ha permesso di utilizzare lo stesso ptr con nomi di membri presi da definizioni di strutture diverse ma con lo stesso offset; i programmatori hanno usato quell'abilità per una varietà di scopi.

Quando ai membri della struttura venivano assegnati i propri spazi dei nomi, divenne impossibile dichiarare due membri della struttura con lo stesso spostamento. L'aggiunta di sindacati alla lingua ha reso possibile ottenere la stessa semantica che era stata disponibile nelle versioni precedenti della lingua (sebbene l'incapacità di avere nomi esportati in un contesto accluso potrebbe aver ancora reso necessario l'uso di un comando trova / sostituisci per sostituire il membro> in foo-> type1.member). Ciò che era importante non era tanto che le persone che avevano aggiunto i sindacati avessero in mente un particolare utilizzo del target, ma piuttosto che fornivano un mezzo con cui i programmatori che avevano fatto affidamento sulla semantica precedente, per qualunque scopo , avrebbero potuto comunque essere in grado di raggiungere il stessa semantica anche se dovevano usare una sintassi diversa per farlo.


Apprezzo la lezione di storia, tuttavia con lo standard che definisce tale e come non definito, che non era il caso nell'era C passata in cui il libro di K&R era l'unico "standard", bisogna essere sicuri di non usarlo per qualsiasi scopo e entra nella terra UB.
legends2k,

2
@ legends2k: quando è stato redatto lo Standard, la maggior parte delle implementazioni in C trattava i sindacati allo stesso modo e tale trattamento era utile. Alcuni, tuttavia, non lo fecero, e gli autori della norma detestavano detestare qualsiasi implementazione esistente come "non conforme". Invece, hanno pensato che se gli implementatori non avessero avuto bisogno dello Standard per dire loro di fare qualcosa (come evidenziato dal fatto che lo stavano già facendo ), lasciarlo non specificato o indefinito avrebbe semplicemente preservato lo status quo . L'idea che dovrebbe rendere le cose meno definite di quanto non fossero prima della
stesura

2
... sembra un'innovazione molto più recente. La cosa particolarmente triste di tutto ciò è che se gli autori di compilatori destinati ad applicazioni di fascia alta dovessero capire come aggiungere utili direttive di ottimizzazione al linguaggio che la maggior parte dei compilatori ha implementato negli anni '90, piuttosto che mettere a disposizione funzionalità e garanzie che erano state supportate da "solo "90% delle implementazioni, il risultato sarebbe un linguaggio che potrebbe funzionare meglio e in modo più affidabile dell'ipermoderna C.
supercat

2

È possibile utilizzare un sindacato per due motivi principali:

  1. Un modo pratico per accedere agli stessi dati in diversi modi, come nel tuo esempio
  2. Un modo per risparmiare spazio quando ci sono diversi membri di dati di cui solo uno può essere "attivo"

1 È davvero più un hack in stile C per abbreviare la scrittura del codice sulla base della conoscenza dell'architettura di memoria del sistema di destinazione. Come già detto, di solito puoi cavartela se in realtà non scegli come target molte piattaforme diverse. Credo che alcuni compilatori potrebbero permetterti di usare anche le direttive sull'imballaggio (so che lo fanno sulle strutture)?

Un buon esempio di 2. può essere trovato nel tipo VARIANT ampiamente utilizzato in COM.


2

Come altri hanno già detto, i sindacati combinati con le enumerazioni e inseriti in strutture possono essere utilizzati per implementare i sindacati con tag. Un uso pratico è implementare Rust Result<T, E>, che è originariamente implementato usando un puro enum(Rust può contenere dati aggiuntivi nelle varianti di enumerazione). Ecco un esempio in C ++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
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.