¿Cómo se pueden almacenar los tipos de datos mixtos (int, float, char, etc.) en una matriz?

145

Quiero almacenar tipos de datos mixtos en una matriz. ¿Cómo podría uno hacer eso?

chanzerre
fuente
8
Es posible y hay casos de uso, pero este es probablemente un diseño defectuoso. Para eso no están las matrices.
djechlin

Respuestas:

244

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 typemiembro se usa para mantener la elección de qué miembro de la uniondebe usarse para cada elemento de la matriz. Entonces, si desea almacenar un inten 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 switchdeclaració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 typemiembro siempre corresponda al último valor almacenado en el union.

Barmar
fuente
23
+1 Esta es la implementación de muchos lenguajes de interpretación escritos en C
texasbruce
8
@texasbruce también se llama una "unión etiquetada". Estoy usando esta técnica también en mi propio idioma. ;)
Wikipedia utiliza una página de desambiguación para " unión discriminada " - "unión disjunta" en teoría de conjuntos y, como mencionó @ H2CO3, "unión etiquetada" en informática.
Izkata
14
Y la primera línea de la página de Unión Etiquetada de Wikipedia dice: En informática, una unión etiquetada, también llamada variante, registro de variante, unión discriminada, unión disjunta o tipo de suma, ... Se ha reinventado tantas veces que tiene muchas nombres (como diccionarios, hashes, matrices asociativas, etc.).
Barmar
1
@Barmar Lo reescribí como "unión etiquetada" pero luego leí tu comentario. Retrocedí la edición, no quise destrozar tu respuesta.
32

Use una unión:

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

Sin embargo, deberá realizar un seguimiento del tipo de cada elemento.


fuente
21

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.


fuente
8

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 vala 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 .taginicializador 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
luser droog
fuente
.int_.valsin embargo, no alias la misma área porque el compilador sabe que .valestá en un desplazamiento mayor que .tag. ¿Tienes un enlace para más discusión sobre este supuesto problema?
MM
5

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

dzada
fuente
también puede almacenar la información de tipo en el puntero mismo
phuclv
3

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 intdeben ser múltiplos de 4 (suponiendo que intes 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 longe 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.

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

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 doublees 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

phuclv
fuente