Réponses:
Vous pouvez faire des éléments du tableau une union discriminée, c'est-à-dire une union étiquetée .
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
Le type
membre est utilisé pour contenir le choix du membre qui union
doit être utilisé pour chaque élément du tableau. Donc, si vous voulez stocker un int
dans le premier élément, vous feriez:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
Lorsque vous souhaitez accéder à un élément du tableau, vous devez d'abord vérifier le type, puis utiliser le membre correspondant de l'union. Une switch
déclaration est 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
}
Il appartient au programmeur de s'assurer que le type
membre correspond toujours à la dernière valeur stockée dans le fichier union
.
Utilisez un syndicat:
union {
int ival;
float fval;
void *pval;
} array[10];
Vous devrez cependant garder une trace du type de chaque élément.
Les éléments de tableau doivent avoir la même taille, c'est pourquoi ce n'est pas possible. Vous pouvez contourner ce problème en créant un type de 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 taille de l'élément de l'union est la taille du plus grand élément, 4.
Il existe un style différent de définition du tag-union (quel que soit le nom) que l'OMI rend beaucoup plus agréable à utiliser , en supprimant l'union interne. C'est le style utilisé dans le système X Window pour des choses comme les événements.
L'exemple de la réponse de Barmar donne le nom val
à l'union interne. L'exemple de la réponse de Sp. utilise une union anonyme pour éviter d'avoir à spécifier le .val.
chaque fois que vous accédez à l'enregistrement de variante. Malheureusement, les structures et unions internes "anonymes" ne sont pas disponibles dans C89 ou C99. C'est une extension de compilateur, et donc par nature non portable.
Une meilleure façon de l'OMI est d'inverser toute la définition. Faites de chaque type de données sa propre structure et placez la balise (spécificateur de type) dans chaque structure.
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
Ensuite, vous les enveloppez dans une union au plus haut niveau.
typedef union {
int tag;
integer int_;
real real_;
} record;
enum types { INVALID, INT, REAL };
Maintenant, il peut sembler que nous nous répétons, et nous le sommes . Mais considérez que cette définition est susceptible d'être isolée dans un seul fichier. Mais nous avons éliminé le bruit lié à la spécification de l'intermédiaire .val.
avant d'accéder aux données.
record i;
i.tag = INT;
i.int_.val = 12;
record r;
r.tag = REAL;
r.real_.val = 57.0;
Au lieu de cela, il va à la fin, où c'est moins odieux. :RÉ
Une autre chose que cela permet est une forme d'héritage. Edit: cette partie n'est pas le C standard, mais utilise une extension 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;
Montée et descente.
Edit: Une chose à savoir est si vous en construisez un avec des initialiseurs désignés C99. Tous les initialiseurs de membre doivent passer par le même membre du syndicat.
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
L' .tag
initialiseur peut être ignoré par un compilateur d'optimisation, car l' .int_
initialiseur qui suit les alias utilise la même zone de données. Même si nous connaissons la mise en page (!), Et cela devrait être correct. Non, ce n'est pas le cas. Utilisez plutôt la balise "internal" (elle recouvre la balise externe, comme nous le voulons, mais ne confond pas le compilateur).
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
n'alias pas la même zone car le compilateur sait qu'il .val
s'agit d'un décalage supérieur à .tag
. Avez-vous un lien vers une discussion plus approfondie sur ce problème allégué?
Vous pouvez créer un void *
tableau, avec un tableau séparé de size_t.
Mais vous perdez le type d'information.
Si vous avez besoin de conserver le type d'informations d'une manière ou d'une autre, conservez un troisième tableau de int (où int est une valeur énumérée), puis codez la fonction qui caste en fonction de la enum
valeur.
L'union est la voie à suivre. Mais vous avez également d'autres solutions. L'un de ceux-ci est le pointeur étiqueté , qui consiste à stocker plus d'informations dans les bits "libres" d'un pointeur.
Selon les architectures, vous pouvez utiliser les bits faibles ou élevés, mais le moyen le plus sûr et le plus portable consiste à utiliser les bits faibles inutilisés en tirant parti de la mémoire alignée. Par exemple dans les systèmes 32 bits et 64 bits, les pointeurs vers int
doivent être des multiples de 4 (en supposant qu'il int
s'agit d'un type 32 bits) et les 2 bits les moins significatifs doivent être 0, vous pouvez donc les utiliser pour stocker le type de vos valeurs . Bien sûr, vous devez effacer les bits de balise avant de déréférencer le pointeur. Par exemple, si votre type de données est limité à 4 types différents, vous pouvez l'utiliser comme ci-dessous
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;
}
Si vous pouvez vous assurer que les données sont alignées sur 8 octets (comme pour les pointeurs dans les systèmes 64 bits, ou long long
et uint64_t
...), vous aurez un bit de plus pour la balise.
Cela présente un inconvénient: vous aurez besoin de plus de mémoire si les données n'ont pas été stockées dans une variable ailleurs. Par conséquent, si le type et la plage de vos données sont limités, vous pouvez stocker les valeurs directement dans le pointeur. Cette technique a été utilisée dans la version 32 bits du moteur V8 de Chrome , où il vérifie le bit le moins significatif de l'adresse pour voir s'il s'agit d'un pointeur vers un autre objet (comme un double, de gros entiers, une chaîne ou un objet) ou un 31 valeur signée -bit (appelée smi
- petit entier ). Si c'est un int
, Chrome effectue simplement un décalage arithmétique à droite de 1 bit pour obtenir la valeur, sinon le pointeur est déréférencé.
Sur la plupart des systèmes 64 bits actuels, l'espace d'adressage virtuel est encore beaucoup plus étroit que 64 bits, par conséquent les bits les plus significatifs peuvent également être utilisés comme balises . En fonction de l'architecture, vous avez différentes manières de les utiliser comme balises. ARM , 68k et bien d'autres peuvent être configurés pour ignorer les bits supérieurs , vous permettant de les utiliser librement sans vous soucier de segfault ou quoi que ce soit. De l'article Wikipédia lié ci-dessus:
Un exemple significatif de l'utilisation de pointeurs balisés est le runtime Objective-C sur iOS 7 sur ARM64, notamment utilisé sur l'iPhone 5S. Dans iOS 7, les adresses virtuelles sont de 33 bits (alignés sur les octets), donc les adresses alignées sur les mots n'utilisent que 30 bits (les 3 bits les moins significatifs sont 0), laissant 34 bits pour les balises. Les pointeurs de classe Objective-C sont alignés sur les mots et les champs de balise sont utilisés à de nombreuses fins, telles que le stockage d'un nombre de références et la question de savoir si l'objet a un destructeur.
Les premières versions de MacOS utilisaient des adresses étiquetées appelées Handles pour stocker les références aux objets de données. Les bits hauts de l'adresse indiquaient si l'objet de données était verrouillé, purgeable et / ou provenait d'un fichier de ressources, respectivement. Cela provoquait des problèmes de compatibilité lorsque l'adressage MacOS passait de 24 bits à 32 bits dans System 7.
Sur x86_64, vous pouvez toujours utiliser les bits hauts comme balises avec précaution . Bien sûr, vous n'avez pas besoin d'utiliser tous ces 16 bits et pouvez omettre certains bits pour une preuve future
Dans les versions précédentes de Mozilla Firefox, ils utilisent également de petites optimisations d'entiers comme la V8, avec les 3 bits faibles utilisés pour stocker le type (int, chaîne, objet ... etc.). Mais depuis JägerMonkey, ils ont emprunté un autre chemin ( la nouvelle représentation de la valeur JavaScript de Mozilla , lien de sauvegarde ). La valeur est désormais toujours stockée dans une variable à double précision 64 bits. Lorsque le double
est normalisé , il peut être utilisé directement dans les calculs. Cependant, si les 16 bits supérieurs de celui-ci sont tous des 1, qui désignent un NaN , les 32 bits inférieurs stockeront l'adresse (dans un ordinateur 32 bits) à la valeur ou la valeur directement, les 16 bits restants seront utilisés pour stocker le type. Cette technique s'appelle NaN-boxingou nun-boxe. Il est également utilisé dans JavaScriptCore 64 bits de WebKit et SpiderMonkey de Mozilla avec le pointeur stocké dans les 48 bits bas. Si votre type de données principal est à virgule flottante, c'est la meilleure solution et offre de très bonnes performances.
En savoir plus sur les techniques ci-dessus: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations