Come può un tipo di dati misti (int, float, char, ecc.) Essere memorizzato in un array?

Voglio memorizzare tipi di dati misti in un array. Come si potrebbe farlo?

È ansible rendere gli elementi dell’array un sindacato discriminato, noto anche come unione .

struct { enum { is_int, is_float, is_char } type; union { int ival; float fval; char cval; } val; } my_array[10]; 

Il membro del type viene utilizzato per contenere la scelta di quale membro del union deve essere utilizzato per ogni elemento dell’array. Quindi se vuoi memorizzare un int nel primo elemento, dovresti fare:

 my_array[0].type = is_int; my_array[0].val.ival = 3; 

Quando si desidera accedere a un elemento dell’array, è necessario innanzitutto controllare il tipo, quindi utilizzare il membro corrispondente dell’unione. Un’istruzione switch è utile:

 switch (my_array[n].type) { case is_int: // Do stuff for integer, using my_array[n].ival break; case is_float: // Do stuff for float, using my_array[n].fval break; case is_char: // Do stuff for char, using my_array[n].cvar break; default: // Report an error, this shouldn't happen } 

Viene lasciato al programmatore per assicurarsi che il membro del type corrisponda sempre all’ultimo valore memorizzato union .

Usa un sindacato:

 union { int ival; float fval; void *pval; } array[10]; 

Dovrai comunque tenere traccia del tipo di ciascun elemento.

Gli elementi dell’array devono avere le stesse dimensioni, ecco perché non è ansible. Puoi risolvere il problema creando un tipo di variante :

 #include  #define SIZE 3 typedef enum __VarType { V_INT, V_CHAR, V_FLOAT, } VarType; typedef struct __Var { VarType type; union { int i; char c; float f; }; } Var; void var_init_int(Var *v, int i) { v->type = V_INT; v->i = i; } void var_init_char(Var *v, char c) { v->type = V_CHAR; v->c = c; } void var_init_float(Var *v, float f) { v->type = V_FLOAT; v->f = f; } int main(int argc, char **argv) { Var v[SIZE]; int i; var_init_int(&v[0], 10); var_init_char(&v[1], 'C'); var_init_float(&v[2], 3.14); for( i = 0 ; i < SIZE ; i++ ) { switch( v[i].type ) { case V_INT : printf("INT %d\n", v[i].i); break; case V_CHAR : printf("CHAR %c\n", v[i].c); break; case V_FLOAT: printf("FLOAT %f\n", v[i].f); break; } } return 0; } 

La dimensione dell'elemento dell'unione è la dimensione dell'elemento più grande, 4.

C’è uno stile diverso di definire il tag-union (con qualsiasi nome) che IMO rende molto più piacevole da usare rimuovendo l’unione interna. Questo è lo stile utilizzato nel sistema X Window per cose come Eventi.

L’esempio nella risposta di Barmar dà il nome val all’unione interna. L’esempio nella risposta di Sp. Usa un’unione anonima per evitare di dover specificare il valore .val. ogni volta che accedi al record della variante. Sfortunatamente le strutture e i sindacati interni “anonimi” non sono disponibili in C89 o C99. È un’estensione del compilatore e quindi intrinsecamente non portatile.

Un modo migliore IMO è di invertire l’intera definizione. Assegna a ogni tipo di dati la propria struttura e inserisci il tag (identificatore di tipo) in ogni struttura.

 typedef struct { int tag; int val; } integer; typedef struct { int tag; float val; } real; 

Quindi li avvolgi in un sindacato di alto livello.

 typedef union { int tag; integer int_; real real_; } record; enum types { INVALID, INT, REAL }; 

Ora può sembrare che ci stiamo ripetendo, e lo siamo . Ma considera che questa definizione potrebbe essere isolata in un singolo file. Ma abbiamo eliminato il rumore di specificare il valore intermedio .val. prima di arrivare ai dati.

 record i; i.tag = INT; i.int_.val = 12; record r; r.tag = REAL; r.real_.val = 57.0; 

Invece, va alla fine, dove è meno odioso. : D

Un’altra cosa che consente è una forma di ereditarietà. Modifica: questa parte non è C standard, ma usa un’estensione GNU.

 if (r.tag == INT) { integer x = r; x.val = 36; } else if (r.tag == REAL) { real x = r; x.val = 25.0; } integer g = { INT, 100 }; record rg = g; 

Up-casting e down-casting.


Modifica: una cosa che devi sapere è se stai costruendo uno di questi con gli inizializzatori designati C99. Tutti gli inizializzatori dei membri devono passare attraverso lo stesso membro del sindacato.

 record problem = { .tag = INT, .int_.val = 3 }; problem.tag; // may not be initialized 

L’inizializzatore .tag può essere ignorato da un compilatore ottimizzatore, perché l’inizializzatore .int_ che segue alias la stessa area dati. Anche se conosciamo il layout (!), Dovrebbe essere ok. No, non lo è. Usa invece il tag “interno” (si sovrappone al tag esterno, proprio come vogliamo, ma non confonde il compilatore).

 record not_a_problem = { .int_.tag = INT, .int_.val = 3 }; not_a_problem.tag; // == INT 

Puoi fare un array void * , con una matrice separata di size_t. Ma tu perdi il tipo di informazione.
Se è necessario mantenere il tipo di informazione in qualche modo, mantenere un terzo array di int (dove int è un valore enumerato) Quindi codificare la funzione che esegue il cast in base al valore enum .

L’unione è il modo standard per andare. Ma hai anche altre soluzioni. Uno di questi è un puntatore con tag , che comporta l’archiviazione di maggiori informazioni nei bit “liberi” di un puntatore.

A seconda delle architetture è ansible utilizzare i bit basso o alto, ma il modo più sicuro e più portatile è utilizzare i bit bassi inutilizzati sfruttando la memoria allineata. Ad esempio nei sistemi a 32 e 64 bit, i puntatori a int devono essere multipli di 4 e i 2 bit meno significativi devono essere 0, quindi è ansible utilizzarli per memorizzare il tipo di valori. Ovviamente è necessario cancellare i bit del tag prima di dereferenziare il puntatore. Ad esempio, se il tuo tipo di dati è limitato a 4 tipi diversi, puoi utilizzarlo come di seguito

 void* tp; // tagged pointer enum { is_int, is_double, is_char_p, is_char } type; // ... intptr_t addr = (intptr_t)tp & ~0x03; // clear the 2 low bits in the pointer switch ((intptr_t)tp & 0x03) // check the tag (2 low bits) for the type { case is_int: // data is int printf("%d\n", *((int*)addr)); break; case is_double: // data is double printf("%f\n", *((double*)addr)); break; case is_char_p: // data is char* printf("%s\n", (char*)addr); break; case is_char: // data is char printf("%c\n", *((char*)addr)); break; } 

Se puoi assicurarti che i dati siano allineati a 8 byte (come per i puntatori nei sistemi a 64 bit, o long long e uint64_t …), avrai un altro bit per il tag.

Questo ha uno svantaggio che avrai bisogno di più memoria se i dati non sono stati memorizzati in una variabile altrove. Pertanto, se il tipo e l’intervallo dei dati sono limitati, è ansible memorizzare i valori direttamente nel puntatore. Questa tecnica è stata utilizzata nella versione a 32 bit del motore V8 di Chrome , dove controlla il bit meno significativo dell’indirizzo per vedere se si tratta di un puntatore a un altro object (come il doppio, i grandi numeri interi, una stringa o qualche object) o un 31 -bit valore firmato (chiamato smi – piccolo intero ). Se è un int , Chrome esegue semplicemente uno spostamento aritmetico di 1 bit per ottenere il valore, altrimenti il ​​puntatore è dereferenziato.


Sulla maggior parte degli attuali sistemi a 64 bit lo spazio di indirizzamento virtuale è ancora molto inferiore a 64 bit, quindi il più alto dei bit più significativi può anche essere usato come tag . A seconda dell’architettura, hai diversi modi di usarli come tag. ARM , 68k e molti altri ti permettono di ignorare i bit più in alto, quindi puoi usarli liberamente senza preoccuparti di segfault o altro. Dall’articolo wikipedia collegato sopra:

Un esempio significativo dell’utilizzo di puntatori con tag è il runtime Objective-C su iOS 7 su ARM64, in particolare su iPhone 5S. In iOS 7, gli indirizzi virtuali sono 33 bit (allineati in byte), quindi gli indirizzi allineati alle parole utilizzano solo 30 bit (3 bit meno significativi sono 0), lasciando 34 bit per i tag. I puntatori di class Objective-C sono allineati a parole e i campi tag vengono utilizzati per molti scopi, come la memorizzazione di un conteggio di riferimento e se l’object ha un distruttore. [2] [3]

Le prime versioni di MacOS utilizzavano indirizzi con tag chiamati Handles per memorizzare i riferimenti agli oggetti dati. I bit alti dell’indirizzo indicano se l’object dati è stato bloccato, purgeable e / o originato da un file di risorse, rispettivamente. Ciò ha causato problemi di compatibilità quando l’indirizzamento MacOS avanzato da 24 bit a 32 bit in System 7.

Su x86_64 puoi ancora usare i bit alti come tag con cura . Ovviamente non è necessario utilizzare tutti quei 16 bit e lasciare alcuni bit per prove future

Nelle versioni precedenti di Mozilla Firefox usano anche piccole ottimizzazioni intere come V8, con i 3 bit bassi usati per memorizzare il tipo (int, string, object … ecc.). Ma da quando JägerMonkey hanno intrapreso un altro percorso ( Nuova rappresentazione del valore JavaScript di Mozilla , link di backup ). Il valore è ora sempre memorizzato in una variabile a doppia precisione a 64 bit. Quando il double è normalizzato , può essere utilizzato direttamente nei calcoli. Tuttavia se i 16 bit alti di esso sono tutti 1, che denotano un NaN , i 32 bit bassi memorizzeranno l’indirizzo (in un computer a 32 bit) sul valore o direttamente sul valore, verranno utilizzati i 16 bit rimanenti per memorizzare il tipo. Questa tecnica è chiamata NaN-boxing o nun-boxing. Viene utilizzato anche in JavaScriptCore di WebKit a 64 bit e SpiderMonkey di Mozilla con il puntatore che viene archiviato nei 48 bit bassi. Se il tuo tipo di dati principale è a virgola mobile, questa è la soluzione migliore e offre prestazioni molto buone.

Maggiori informazioni sulle tecniche di cui sopra: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations