Domanda

Ho usato comodamente i sindacati in precedenza;oggi mi sono allarmato quando ho letto questo post e sono venuto a sapere che questo code

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, ad es.leggere da un membro del sindacato diverso da quello a cui si è recentemente scritto porta a un comportamento indefinito.Se questo non è l'uso previsto dei sindacati, qual è?Qualcuno può spiegarlo in modo elaborato?

Aggiornamento:

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 e ignorante lo ha etichettato sia come C che come C++.
  • Dopo aver esaminato lo standard di C++ 11, non ho potuto affermare in modo definitivo che richiama l'accesso/ispezione di un membro dell'unione 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 di membri della struttura con 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 uno o più membri iniziali.

  • Mentre in C, (C99TC3-DR283 in poi) è legale farlo (grazie a Pascal Cuoq per aver sollevato questa questione).Tuttavia, tentando di farlo può ancora portare a comportamenti indefiniti, se il valore letto risulta non essere valido (la cosiddetta "rappresentazione trap") per il tipo tramite cui viene letto.Altrimenti, il valore letto è definito dall'implementazione.
  • C89/90 lo ha sottolineato sotto un comportamento non specificato (Allegato J) e il libro di K&R afferma che l'implementazione è definita.Citazione da K&R:

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

  • Estratto dal TC++PL di Stroustrup (sottolineatura mia)

    L’uso delle unioni può essere essenziale per la compatibilità dei dati [...] a volte usato impropriamente per "conversione di tipo".".

Soprattutto, questa domanda (il cui titolo rimane invariato rispetto alla mia domanda) è stata posta con l'intento di comprendere lo scopo dei sindacati E non su ciò che la norma consente Per esempio.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.

È stato utile?

Soluzione

Lo scopo dei sindacati è piuttosto evidente, ma per qualche motivo la gente perdere abbastanza spesso.

Lo scopo di unione è per salvare la memoria utilizzando la stessa regione di memoria per l'archiviazione di oggetti diversi in momenti diversi. Questo è tutto.

E 'come una stanza in un albergo. Diverse persone vivono in esso per periodi non sovrapposti di tempo. Queste persone non si incontrano mai, e in genere non sanno nulla gli uni degli altri. Gestendo correttamente il time-sharing delle camere (cioè facendo in modo che diverse persone non vengono assegnati a una stanza, allo stesso tempo), un hotel relativamente piccolo può fornire alloggi a un numero relativamente elevato di persone, che è quello che gli alberghi sono per.

Questo è esattamente ciò che l'unione fa. Se si sa che diversi oggetti nel vostro programma di tenere i valori con non sovrapposte di valore vite, allora si può "fondere" questi oggetti in un'unione e quindi salvare la memoria. Proprio come una camera d'albergo ha al massimo un inquilino "attivo" in ogni momento del tempo, un sindacato ha al massimo un elemento "attivo" in ogni momento della durata del programma. Solo il membro "attivo" può essere letto. Scrivendo in altri Stati si cambia lo stato "attivo" a quello degli altri membri.

Per qualche ragione, questo scopo originale del sindacato ha fatto "sovrascritti" con qualcosa di completamente diverso: la scrittura un membro di un sindacato e poi ispezione attraverso un altro membro. Questo tipo di reinterpretazione di memoria (alias "tipo giochi di parole") è non un valido utilizzo dei sindacati. Essa comporta generalmente un comportamento indefinito è descritto come produrre un comportamento attuazione definiti in C89 / 90.

Modifica Utilizzo sindacati ai fini del tipo di giochi di parole (cioè scrivendo un membro e quindi la lettura di un altro) è stato dato una definizione più dettagliata in una delle Rettifiche tecnico allo standard C99 (vedi DR # 257 e DR # 283 ). Tuttavia, tenere presente che formalmente questo non protegge da incorrere in un comportamento indefinito dal tentativo di leggere una rappresentazione trappola.

Altri suggerimenti

Si potrebbe utilizzare i sindacati per creare le strutture come il seguente, che contiene un campo che ci dice quale componente del sindacato è effettivamente utilizzato:

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

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

Il comportamento non è definito dal punto di vista linguistico. Si consideri che le piattaforme diverse possono avere diversi vincoli in allineamento memoria e endianness. Il codice in una grande endian contro una macchina little endian aggiornerà i valori nella struct diverso. Fissaggio del comportamento nella lingua richiederebbe tutte le implementazioni di utilizzare lo stesso endian (e dei vincoli di allineamento della memoria ...) che limita l'utilizzo.

Se si utilizza C ++ (che si sta utilizzando due tag) e davvero a cuore la portabilità, allora si può solo utilizzare la struct e fornire un setter che prende il uint32_t e imposta i campi in modo appropriato attraverso operazioni di maschera di bit. Lo stesso può essere fatto in C con una funzione.

Modifica : Mi aspettavo AProgrammer di scrivere una risposta a votare e chiudere questo. Come alcuni commenti hanno sottolineato, endianness è trattata in altre parti della norma lasciando ogni implementazione decidere cosa fare, e l'allineamento e imbottitura può anche essere gestita in modo diverso. Ora, le rigide regole di aliasing che AProgrammer si riferisce implicitamente sono un punto importante qui. Il compilatore è permesso di fare ipotesi sulla modifica (o mancanza di modifica) delle variabili. Nel caso del sindacato, il compilatore potrebbe riordinare le istruzioni e spostare la lettura di ogni componente di colore sopra la scrittura alla variabile colore.

Il più comune uso di union vengo regolarmente tutto è aliasing .

Si consideri il seguente:

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

Cosa fare? Permette pulita, ordinata l'accesso dei membri di un Vector3f vec; da o Nome:

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

o mediante accesso intero nella matrice

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

In alcuni casi, l'accesso in base al nome è la cosa più chiara che si può fare. In altri casi, soprattutto quando l'asse viene scelto programmazione, la cosa più facile da fare è accedere asse mediante indice numerico -. 0 per x, 1 per y, e 2 per z

Come dici tu, questo è rigorosamente un comportamento indefinito, anche se sarà "lavoro" su molte piattaforme. La vera ragione per l'utilizzo di sindacati è quello di creare record variante.

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, è anche bisogno di una sorta di discriminatore di dire ciò che la variante contiene in realtà. E notare che in C ++ sindacati sono non serve a molto perché possono contenere solo i tipi POD -. Efficacemente coloro che non hanno costruttori e distruttori

In C è stato un bel modo per implementare qualcosa di simile a 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 memoria terrazzino 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 di bit.

Anche se questo è strettamente un comportamento indefinito, in pratica, che possa funzionare con praticamente qualsiasi compilatore. Si tratta di un paradigma come ampiamente diffuso che qualsiasi compilatore che si rispetti avrà bisogno di fare "la cosa giusta" in casi come questo. E 'sicuramente da preferire tipo-giochi di parole, che potrebbe generare il codice rotto con alcuni compilatori.

In C ++, Boost Variante implementare una cassetta di sicurezza la versione del sindacato, progettato per impedire un comportamento indefinito, per quanto possibile.

Le sue prestazioni sono identiche al costrutto enum + union (stack allocato troppo etc), ma utilizza un elenco di modelli di tipi posto del enum:)

Il comportamento può essere indefinito, ma questo significa solo che non c'è un "standard". Tutti i compilatori decenti offrono #pragmas per controllare imballaggio e l'allineamento, ma può avere diverse impostazioni predefinite. Le impostazioni predefinite cambieranno anche a seconda delle impostazioni di ottimizzazione utilizzate.

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

tecnicamente è definito, ma in realtà la maggior parte (tutte?) Compilatori trattarlo esattamente come con un reinterpret_cast da un tipo all'altro, il cui risultato è la realizzazione definito. Non vorrei perdere il sonno sopra il vostro codice attuale.

Per un altro esempio di uso effettivo dei sindacati, il quadro CORBA serializza gli oggetti che utilizzano il metodo unione con tag. Tutte le classi definite dall'utente sono membri di una (enorme) unione, e racconta la demarshaller come interpretare il sindacato.

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

Ho letto che il problema in quanto la memoria per le variabili è condivisa, quindi scrivendo a uno, gli altri cambiano e, a seconda del loro tipo, il valore potrebbe essere privo di significato.

ad es.     unione{       float f;       int i;     } X;

La scrittura di x.i non avrebbe senso se si invita a leggere dalla x.f -. A meno che questo è ciò che si intende per guardare ai componenti segno, esponente o mantissa del galleggiante

Credo che ci sia anche un problema di allineamento: se alcune variabili devono essere allineati parola, allora non si potrebbe ottenere il risultato previsto

.

ad es.     unione{       char c [4];       int i;     } X;

Se, per ipotesi, su una macchina un char doveva essere allineato parola poi c [0] c [1] dovrebbero condividere lo storage con i ma non c [2] e C [3].

Nel linguaggio C come è stato documentato nel 1974, tutti i membri della struttura hanno condiviso uno spazio dei nomi comuni, e il significato di "membro ptr->" è stato definito come l'aggiunta del spostamento del membro di "PTR" e accedendo all'indirizzo risultante utilizzando la il tipo di utente. Questo design ha permesso di utilizzare lo stesso PTR con membro nomi presi da diverse definizioni di strutture, ma con lo stesso offset; programmatori utilizzato che la capacità per una varietà di scopi.

Quando i membri della struttura sono stati assegnati i propri spazi dei nomi, è diventato impossibile dichiarare due membri di struttura con la stessa cilindrata. Aggiunta sindacati il linguaggio ha permesso di raggiungere la stessa semantica che erano state disponibile nelle versioni precedenti del linguaggio (anche se l'incapacità di avere nomi esportati in un contesto che racchiude potrebbero essere ancora reso necessario utilizzare un ricerca / sostituzione per sostituire foo-> membro in foo-> type1.member). Cosa era importante non era tanto che la gente che hanno aggiunto i sindacati hanno una particolare indirizzare l'utilizzo in mente, ma piuttosto che essi forniscono un mezzo attraverso il quale i programmatori che aveva invocato la semantica precedenti, per qualsiasi scopo , dovrebbe essere ancora essere in grado di raggiungere la stessa semantica, anche se dovessero usare un diverso sintassi per farlo.

È possibile usare a un sindacato per due motivi principali:

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

1 è davvero più di un hack C-stile di scrittura di codice scorciatoia sulla base di sapere come funziona l'architettura di memoria del sistema di destinazione. Come già detto in genere si può farla franca, se in realtà non bersaglio un sacco di differenti piattaforme. Credo che alcuni compilatori potrebbero consentire di utilizzare le direttive di imballaggio anche (lo so che fanno su struct)?

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

Come altri hanno già detto, le unioni combinate con enumerazioni e racchiuse in strutture possono essere utilizzate per implementare unioni con tag.Un uso pratico è implementare quello di Rust Result<T, E>, che è stato originariamente implementato utilizzando un metodo pure enum (Rust può contenere dati aggiuntivi nelle varianti di enumerazione).Ecco un esempio 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;
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top