Risposte:
È possibile rendere gli elementi dell'array un'unione discriminata, ovvero unione contrassegnata .
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
Il type
membro viene utilizzato per mantenere la scelta di quale membro union
dell'elemento deve essere utilizzato per ciascun elemento dell'array. Quindi, se si desidera memorizzare un int
nel primo elemento, si dovrebbe fare:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
Quando si desidera accedere a un elemento dell'array, è necessario prima verificare il tipo, quindi utilizzare il membro corrispondente dell'unione. Una switch
dichiarazione è 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
}
Spetta al programmatore assicurarsi che il type
membro corrisponda sempre all'ultimo valore memorizzato in union
.
Usa un'unione:
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 è possibile. Puoi aggirarlo creando un tipo di variante :
#include <stdio.h>
#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.
Esiste uno stile diverso di definizione del 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
al sindacato interno. L'esempio nella risposta di Sp. Usa un'unione anonima per evitare di dover specificare.val.
ogni volta che si accede al record della variante. Purtroppo 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 per IMO è di invertire l'intera definizione. Rendi ogni tipo di dati la sua struttura e inserisci il tag (identificatore del tipo) in ogni struttura.
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
Quindi li avvolgi in un'unione 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 . Tuttavia, è probabile che questa definizione sia probabilmente isolata in un singolo file. Ma abbiamo eliminato il rumore di specificare l'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 eredità. Modifica: questa parte non è standard C, ma utilizza 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: un aspetto da tenere presente è se stai costruendo uno di questi con inizializzatori designati C99. Tutti gli inizializzatori dei membri devono essere tramite lo stesso membro del sindacato.
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
L' .tag
inizializzatore può essere ignorato da un compilatore di ottimizzazione, poiché l' .int_
inizializzatore che segue alias identifica la stessa area dati. Anche se noi conosciamo il layout (!), E dovrebbe essere ok. No, non lo è. Utilizzare invece il tag "interno" (sovrappone il 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
.int_.val
non alias la stessa area però perché il compilatore sa che ha .val
un offset maggiore di .tag
. Hai un link per ulteriori discussioni su questo presunto problema?
Puoi fare un void *
array, con un array separato di size_t.
Ma perdi il tipo di informazione.
Se è necessario mantenere il tipo di informazioni in qualche modo, mantenere un terzo array di int (dove int è un valore enumerato) Quindi codificare la funzione che viene lanciata in base al enum
valore.
L'unione è la strada standard da percorrere. Ma hai anche altre soluzioni. Uno di questi è il puntatore con tag , che comporta la memorizzazione di maggiori informazioni nei bit "liberi" di un puntatore.
A seconda delle architetture è possibile utilizzare i bit bassi o alti, ma il modo più sicuro e portatile è quello di utilizzare i bit bassi non utilizzati sfruttando la memoria allineata. Ad esempio nei sistemi a 32 e 64 bit, i puntatori int
devono essere multipli di 4 (supponendo che int
sia un tipo a 32 bit) e i 2 bit meno significativi devono essere 0, quindi è possibile utilizzarli per memorizzare il tipo di valori . Naturalmente è necessario cancellare i bit dei tag prima di dereferenziare il puntatore. Ad esempio, se il tipo di dati è limitato a 4 tipi diversi, è possibile utilizzarlo come di seguito
void* tp; // tagged pointer
enum { is_int, is_double, is_char_p, is_char } type;
// ...
uintptr_t addr = (uintptr_t)tp & ~0x03; // clear the 2 low bits in the pointer
switch ((uintptr_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 si riesce a fare in modo che i dati è di 8 byte allineato (come per i puntatori nei sistemi a 64 bit, o di long long
e uint64_t
...), avrete un altro po 'per il tag.
Questo ha uno svantaggio che avrai bisogno di più memoria se i dati non sono stati archiviati in una variabile altrove. Pertanto, nel caso in cui il tipo e l'intervallo dei dati siano limitati, è possibile 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 oggetto (come double, big integer, string o some object) o un 31 -bit valore con segno (chiamato smi
- intero piccolo ). In caso int
affermativo, Chrome esegue semplicemente uno spostamento aritmetico a destra di 1 bit per ottenere il valore, altrimenti il puntatore viene differito.
Sulla maggior parte dei sistemi a 64 bit attuali lo spazio di indirizzi virtuali è ancora molto più stretto di 64 bit, quindi i bit più alti e significativi possono essere utilizzati anche come tag . A seconda dell'architettura hai diversi modi per usarli come tag. ARM , 68k e molti altri possono essere configurati per ignorare i bit migliori , permettendoti di usarli liberamente senza preoccuparti di segfault o altro. Dall'articolo Wikipedia collegato sopra:
Un esempio significativo dell'uso dei puntatori con tag è il runtime Objective-C su iOS 7 su ARM64, utilizzato in particolare su iPhone 5S. In iOS 7, gli indirizzi virtuali sono 33 bit (allineati a byte), quindi gli indirizzi allineati a parola usano solo 30 bit (3 bit meno significativi sono 0), lasciando 34 bit per i tag. I puntatori di classe Objective-C sono allineati a parole e i campi tag vengono utilizzati per vari scopi, come la memorizzazione di un conteggio di riferimento e se l'oggetto ha un distruttore.
Le prime versioni di MacOS utilizzavano indirizzi con tag denominati Handles per memorizzare riferimenti a oggetti dati. I bit alti dell'indirizzo indicavano se l'oggetto dati era bloccato, eliminabile e / o originato da un file di risorse, rispettivamente. Ciò ha causato problemi di compatibilità quando l'indirizzamento MacOS è passato da 24 bit a 32 bit in System 7.
Su x86_64 puoi comunque usare i bit alti come tag con cura . Naturalmente non è necessario utilizzare tutti quei 16 bit e è possibile tralasciarne alcuni per prove future
Nelle versioni precedenti di Mozilla Firefox usano anche ottimizzazioni di piccoli numeri interi come V8, con i 3 bit bassi utilizzati per memorizzare il tipo (int, stringa, oggetto ... ecc.). Ma dal momento che JägerMonkey hanno preso un altro percorso ( Nuova rappresentazione del valore JavaScript di Mozilla , collegamento di backup ). Il valore è ora sempre memorizzato in una variabile a precisione doppia a 64 bit. Quando double
è normalizzato , può essere utilizzato direttamente nei calcoli. Tuttavia, se i 16 bit alti sono tutti 1, che indicano un NaN , i 32 bit bassi memorizzeranno l'indirizzo (in un computer a 32 bit) direttamente sul valore o sul valore, verranno utilizzati i restanti 16 bit per memorizzare il tipo. Questa tecnica si chiama NaN-boxingo suora-boxe. Viene anche utilizzato in JavaScriptCore a 64 bit di WebKit e SpiderMonkey di Mozilla con il puntatore memorizzato nei 48 bit bassi. Se il tipo di dati principale è in 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