Come possono essere archiviati in un array tipi di dati misti (int, float, char, ecc.)?


145

Voglio archiviare tipi di dati misti in un array. Come si può farlo?


8
È possibile e ci sono casi d'uso, ma questo è probabilmente un design difettoso. Questo non è ciò a cui servono gli array.
Djechlin,

Risposte:


244

È 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 typemembro viene utilizzato per mantenere la scelta di quale membro uniondell'elemento deve essere utilizzato per ciascun elemento dell'array. Quindi, se si desidera memorizzare un intnel 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 switchdichiarazione è 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 typemembro corrisponda sempre all'ultimo valore memorizzato in union.


23
+1 Questa è la spiegazione di molte lingue interpretative scritte in C
texasbruce il

8
@texasbruce ha anche chiamato "unione taggata". Sto usando questa tecnica anche nella mia lingua. ;)

Wikipedia utilizza una pagina di disambiguazione per " unione discriminata " - "unione disgiunta" nella teoria degli insiemi e, come ha detto @ H2CO3, "unione contrassegnata" nell'informatica.
Izkata,

14
E la prima riga della pagina del sindacato con tag Wikipedia dice: Nell'informatica, un sindacato con tag, chiamato anche una variante, record di variante, unione discriminata, unione disgiunta o tipo di somma, ... È stato reinventato così tante volte che ha molte nomi (tipi di dizionari simili, hash, array associativi, ecc.).
Barmar,

1
@Barmar L'ho riscritto come "tagged union" ma poi ho letto il tuo commento. Rollback della modifica, non intendevo vandalizzare la tua risposta.

32

Usa un'unione:

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

Dovrai comunque tenere traccia del tipo di ciascun elemento.


21

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.


8

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 valal 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' .taginizializzatore 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_.valnon alias la stessa area però perché il compilatore sa che ha .valun offset maggiore di .tag. Hai un link per ulteriori discussioni su questo presunto problema?
MM

5

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 enumvalore.


puoi anche memorizzare le informazioni sul tipo nel puntatore stesso
phuclv,

3

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 intdevono essere multipli di 4 (supponendo che intsia 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 longe 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 intaffermativo, 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.

https://en.wikipedia.org/wiki/Tagged_pointer#Examples

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

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.