Respuestas:
Puede hacer que los elementos de la matriz sean una unión discriminada, también conocida como unión etiquetada .
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
El type
miembro se usa para mantener la elección de qué miembro de la union
debe usarse para cada elemento de la matriz. Entonces, si desea almacenar un int
en el primer elemento, haría:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
Cuando desee acceder a un elemento de la matriz, primero debe verificar el tipo y luego usar el miembro correspondiente de la unión. Una switch
declaración es útil:
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
}
Es responsabilidad del programador asegurarse de que el type
miembro siempre corresponda al último valor almacenado en el union
.
Use una unión:
union {
int ival;
float fval;
void *pval;
} array[10];
Sin embargo, deberá realizar un seguimiento del tipo de cada elemento.
Los elementos de la matriz deben tener el mismo tamaño, por eso no es posible. Podría solucionarlo creando un tipo 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;
}
El tamaño del elemento de la unión es el tamaño del elemento más grande, 4.
Hay un estilo diferente de definir la unión de etiquetas (por cualquier nombre) que IMO hace que sea mucho más agradable de usar , eliminando la unión interna. Este es el estilo utilizado en el sistema X Window para cosas como eventos.
El ejemplo en la respuesta de Barmar le da el nombre val
a la unión interna. El ejemplo en la respuesta de Sp. Utiliza una unión anónima para evitar tener que especificar .val.
cada vez que accede al registro de variantes. Lamentablemente, las estructuras y uniones internas "anónimas" no están disponibles en C89 o C99. Es una extensión del compilador, y por lo tanto inherentemente no portátil.
Una mejor manera de IMO es invertir toda la definición. Haga que cada tipo de datos sea su propia estructura y coloque la etiqueta (especificador de tipo) en cada estructura.
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
Luego los envuelve en una unión de alto nivel.
typedef union {
int tag;
integer int_;
real real_;
} record;
enum types { INVALID, INT, REAL };
Ahora puede parecer que nos estamos repitiendo, y lo estamos . Pero tenga en cuenta que es probable que esta definición esté aislada en un solo archivo. Pero hemos eliminado el ruido de especificar el intermedio .val.
antes de llegar a los datos.
record i;
i.tag = INT;
i.int_.val = 12;
record r;
r.tag = REAL;
r.real_.val = 57.0;
En cambio, va al final, donde es menos desagradable. :RE
Otra cosa que esto permite es una forma de herencia. Editar: esta parte no es C estándar, pero usa una extensión 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 y down-casting.
Editar: Una cosa que debes tener en cuenta es si estás construyendo uno de estos con inicializadores designados C99. Todos los inicializadores de miembros deben ser a través del mismo miembro de la unión.
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
El .tag
inicializador puede ser ignorado por un compilador de optimización, porque el .int_
inicializador que sigue alias la misma área de datos. A pesar de que conocemos la disposición (!), Y que debe estar bien. No, no lo es. Utilice la etiqueta "interna" en su lugar (se superpone a la etiqueta externa, tal como queremos, pero no confunde al compilador).
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
sin embargo, no alias la misma área porque el compilador sabe que .val
está en un desplazamiento mayor que .tag
. ¿Tienes un enlace para más discusión sobre este supuesto problema?
Puede hacer una void *
matriz, con una matriz separada de size_t.
Pero pierde el tipo de información.
Si necesita mantener el tipo de información de alguna manera, mantenga una tercera matriz de int (donde int es un valor enumerado) Luego codifique la función que convierte dependiendo del enum
valor.
La unión es el camino estándar a seguir. Pero también tienes otras soluciones. Uno de ellos es el puntero etiquetado , que implica almacenar más información en los bits "libres" de un puntero.
Dependiendo de las arquitecturas, puede usar los bits bajos o altos, pero la forma más segura y portátil es usar los bits bajos no utilizados aprovechando la memoria alineada. Por ejemplo, en sistemas de 32 bits y 64 bits, los punteros int
deben ser múltiplos de 4 (suponiendo que int
es un tipo de 32 bits) y los 2 bits menos significativos deben ser 0, por lo tanto, puede usarlos para almacenar el tipo de sus valores . Por supuesto, debe borrar los bits de la etiqueta antes de desreferenciar el puntero. Por ejemplo, si su tipo de datos está limitado a 4 tipos diferentes, puede usarlos como se muestra a continuación
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 usted puede asegurarse de que los datos es de 8 bytes alineados (como para los punteros en sistemas de 64 bits, o long long
e uint64_t
...), tendrá un poco más por la etiqueta.
Esto tiene la desventaja de que necesitará más memoria si los datos no se han almacenado en una variable en otro lugar. Por lo tanto, en caso de que el tipo y el rango de sus datos sean limitados, puede almacenar los valores directamente en el puntero. Esta técnica se ha utilizado en la versión de 32 bits de motor V8 Chrome , donde verifica el bit menos significativo de la dirección para ver si es un puntero a otro objeto (como dobles, enteros grandes, cadenas o algún objeto) o un 31 -bit valor con signo (llamado smi
- entero pequeño ). Si es un int
, Chrome simplemente hace un desplazamiento aritmético a la derecha de 1 bit para obtener el valor, de lo contrario, el puntero se desreferencia.
En la mayoría de los sistemas actuales de 64 bits, el espacio de direcciones virtuales sigue siendo mucho más estrecho que 64 bits, de ahí que el más significativos también se pueden usar como etiquetas . Dependiendo de la arquitectura, tiene diferentes formas de usarlas como etiquetas.ARM , 68k y muchos otros se pueden configurar para ignorar los bits superiores , lo que le permite usarlos libremente sin preocuparse por segfault ni nada. Del artículo de Wikipedia vinculado arriba:
Un ejemplo significativo del uso de punteros etiquetados es el tiempo de ejecución de Objective-C en iOS 7 en ARM64, especialmente utilizado en el iPhone 5S. En iOS 7, las direcciones virtuales son de 33 bits (alineadas por bytes), por lo que las direcciones alineadas por palabras solo usan 30 bits (3 bits menos significativos son 0), dejando 34 bits para las etiquetas. Los punteros de clase Objective-C están alineados con palabras, y los campos de etiqueta se usan para muchos propósitos, como almacenar un recuento de referencia y si el objeto tiene un destructor.
Las primeras versiones de MacOS usaban direcciones etiquetadas llamadas Manijas para almacenar referencias a objetos de datos. Los bits altos de la dirección indicaban si el objeto de datos estaba bloqueado, se podía purgar y / o se originó a partir de un archivo de recursos, respectivamente. Esto causó problemas de compatibilidad cuando el direccionamiento de MacOS avanzó de 24 bits a 32 bits en el Sistema 7.
En x86_64 aún puede usar los bits altos como etiquetas con cuidado . Por supuesto, no necesita usar todos esos 16 bits y puede omitir algunos bits para pruebas futuras
En versiones anteriores de Mozilla Firefox también utilizan pequeñas optimizaciones de enteros como V8, con los 3 bits bajos utilizados para almacenar el tipo (int, string, object ... etc.). Pero desde JägerMonkey tomaron otro camino ( Nueva Representación de Valor JavaScript de Mozilla , enlace de respaldo ). El valor ahora siempre se almacena en una variable de doble precisión de 64 bits. Cuando double
es normalizado , se puede usar directamente en los cálculos. Sin embargo, si los 16 bits más altos son todos 1, lo que denota un NaN , los 32 bits inferiores almacenarán la dirección (en una computadora de 32 bits) en el valor o el valor directamente, se utilizarán los 16 bits restantes para almacenar el tipo Esta técnica se llama NaN-boxingo monjas de boxeo. También se usa en JavaScriptCore de WebKit de 64 bits y SpiderMonkey de Mozilla con el puntero almacenado en los 48 bits bajos. Si su tipo de datos principal es de punto flotante, esta es la mejor solución y ofrece un rendimiento muy bueno.
Lea más sobre las técnicas anteriores: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations