Como os tipos de dados mistos (int, float, char, etc) podem ser armazenados em uma matriz?

145

Eu quero armazenar tipos de dados mistos em uma matriz. Como alguém poderia fazer isso?

chanzerre
fonte
8
É possível e há casos de uso, mas esse provavelmente é um design defeituoso. Não é para isso que servem as matrizes.
djechlin

Respostas:

244

Você pode transformar os elementos da matriz em uma união discriminada, também conhecida como união marcada .

struct {
    enum { is_int, is_float, is_char } type;
    union {
        int ival;
        float fval;
        char cval;
    } val;
} my_array[10];

O typemembro é usado para manter a escolha de qual membro do uniondeve ser usado para cada elemento da matriz. Portanto, se você deseja armazenar um intno primeiro elemento, faça:

my_array[0].type = is_int;
my_array[0].val.ival = 3;

Quando você deseja acessar um elemento da matriz, deve primeiro verificar o tipo e, em seguida, usar o membro correspondente da união. Uma switchdeclaração é ú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
}

É responsabilidade do programador garantir que o typemembro sempre corresponda ao último valor armazenado no union.

Barmar
fonte
23
1 Este é o implentation de muitas linguagens de interpretação escritos em C
texasbruce
8
@texasbruce também chamado de "união marcada". Também estou usando essa técnica no meu próprio idioma. ;)
A Wikipedia usa uma página de desambiguação para " união discriminada " - "união desarticulada" na teoria dos conjuntos e, como @ H2CO3 mencionou, "união marcada" na ciência da computação.
Izkata 02/09
14
E a primeira linha da página de união Tagged da Wikipedia diz: Na ciência da computação, uma união marcada, também chamada de variante, registro de variante, união discriminada, união desunida ou tipo de soma, ... Foi reinventada tantas vezes que tem muitas nomes (como dicionários, hashes, matrizes associativas etc.).
Barmar
1
@ Barmar Eu reescrevi como "união marcada", mas depois li o seu comentário. Revertida a edição, não pretendia vandalizar sua resposta.
32

Use uma união:

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

Você precisará acompanhar o tipo de cada elemento.


fonte
21

Os elementos da matriz precisam ter o mesmo tamanho, por isso não é possível. Você pode contornar isso criando um 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;
}

O tamanho do elemento da união é o tamanho do elemento maior, 4.


fonte
8

Há um estilo diferente de definir a união de tags (por qualquer nome) que a IMO torne muito mais agradável o uso , removendo a união interna. Esse é o estilo usado no sistema X Window para coisas como eventos.

O exemplo da resposta de Barmar dá o nome valà união interna. O exemplo na resposta de Sp. Usa uma união anônima para evitar precisar especificar o.val. sempre que você acessar o registro de variante. Infelizmente, estruturas e uniões internas "anônimas" não estão disponíveis em C89 ou C99. É uma extensão do compilador e, portanto, inerentemente não portátil.

Uma maneira melhor da IMO é inverter toda a definição. Torne cada tipo de dado sua própria estrutura e coloque a tag (especificador de tipo) em cada estrutura.

typedef struct {
    int tag;
    int val;
} integer;

typedef struct {
    int tag;
    float val;
} real;

Em seguida, envolva-os em uma união de nível superior.

typedef union {
    int tag;
    integer int_;
    real real_;
} record;

enum types { INVALID, INT, REAL };

Agora, pode parecer que estamos nos repetindo, e nós são . Mas considere que é provável que essa definição seja isolada em um único arquivo. Mas eliminamos o ruído de especificar o intermediário .val.antes de você chegar aos dados.

record i;
i.tag = INT;
i.int_.val = 12;

record r;
r.tag = REAL;
r.real_.val = 57.0;

Em vez disso, vai no final, onde é menos desagradável. : D

Outra coisa que isso permite é uma forma de herança. Editar: esta parte não é padrão C, mas usa uma extensão 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.


Edit: Uma dica para você estar ciente é se você estiver construindo um desses com inicializadores C99 designados. Todos os inicializadores de membros devem passar pelo mesmo membro do sindicato.

record problem = { .tag = INT, .int_.val = 3 };

problem.tag; // may not be initialized

O .taginicializador pode ser ignorado por um compilador de otimização, porque o .int_inicializador que segue aliases a mesma área de dados. Mesmo que nós sabemos o layout (!), E ele deve estar ok. Não, não é. Use a tag "internal" (sobrepõe a tag externa, exatamente como queremos, mas não confunde o compilador).

record not_a_problem = { .int_.tag = INT, .int_.val = 3 };

not_a_problem.tag; // == INT
luser droog
fonte
.int_.valnão alias a mesma área porque o compilador sabe que .valestá em um deslocamento maior que .tag. Você tem um link para uma discussão mais aprofundada sobre esse suposto problema?
MM
5

Você pode fazer uma void *matriz, com uma matriz separada de size_t.Mas você perde o tipo de informação.
Se você precisar manter o tipo de informação de alguma forma, mantenha uma terceira matriz de int (onde int é um valor enumerado). Em seguida, codifique a função que lança dependendo do enumvalor.

dzada
fonte
você também pode armazenar as informações de tipo no ponteiro em si
phuclv
3

A união é o caminho padrão a seguir. Mas você tem outras soluções também. Uma delas é apontada como ponteiro , o que envolve o armazenamento de mais informações nos bits "livres" de um ponteiro.

Dependendo das arquiteturas, você pode usar os bits mais altos ou mais baixos, mas a maneira mais segura e portátil é usar os bits mais baixos não utilizados , aproveitando a vantagem da memória alinhada. Por exemplo, em sistemas de 32 e 64 bits, os ponteiros intdevem ter múltiplos de 4 (assumindo que inté um tipo de 32 bits) e os 2 bits menos significativos devem ser 0, portanto, você pode usá-los para armazenar o tipo de seus valores . Obviamente, você precisa limpar os bits da tag antes de remover a referência do ponteiro. Por exemplo, se seu tipo de dados é limitado a 4 tipos diferentes, você pode usá-lo como abaixo

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 você pode ter certeza de que os dados são 8 bytes alinhados (como por ponteiros em sistemas de 64 bits, ou long longe uint64_t...), você vai ter mais um pouco para o tag.

Isso tem uma desvantagem: você precisará de mais memória se os dados não tiverem sido armazenados em uma variável em outro local. Portanto, caso o tipo e o intervalo dos seus dados sejam limitados, você pode armazenar os valores diretamente no ponteiro. Essa técnica foi usada na versão de 32 bits do mecanismo V8 do Chrome , onde verifica o bit menos significativo do endereço para ver se isso é um ponteiro para outro objeto (como números grandes, inteiros, string ou algum objeto), ou um 31 valor assinado de bits (chamado smi- pequeno inteiro ). Se for um int, o Chrome simplesmente faz um deslocamento aritmético à direita 1 bit para obter o valor, caso contrário, o ponteiro é desreferenciado.


Na maioria dos sistemas atuais de 64 bits, o espaço de endereço virtual ainda é muito mais estreito que 64 bits; portanto, os bits mais significativos também podem ser usados ​​como tags . Dependendo da arquitetura, você tem maneiras diferentes de usá-las como tags. O ARM , 68k e muitos outros podem ser configurados para ignorar os bits superiores , permitindo que você os use livremente sem se preocupar com segfault ou qualquer coisa. No artigo vinculado da Wikipedia acima:

Um exemplo significativo do uso de ponteiros marcados é o tempo de execução do Objective-C no iOS 7 no ARM64, usado principalmente no iPhone 5S. No iOS 7, os endereços virtuais são de 33 bits (alinhados por bytes); portanto, os endereços alinhados por palavras usam apenas 30 bits (3 bits menos significativos são 0), deixando 34 bits para tags. Os ponteiros da classe Objective-C são alinhados por palavras e os campos de tags são usados ​​para muitos propósitos, como armazenar uma contagem de referência e se o objeto tem um destruidor.

As versões anteriores do MacOS usavam endereços marcados chamados Handles para armazenar referências a objetos de dados. Os bits altos do endereço indicavam se o objeto de dados estava bloqueado, eliminável e / ou originado de um arquivo de recurso, respectivamente. Isso causou problemas de compatibilidade quando o endereçamento MacOS avançou de 24 bits para 32 bits no Sistema 7.

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

No x86_64, você ainda pode usar os bits altos como tags com cuidado . Claro que você não precisa usar todos esses 16 bits e pode deixar alguns bits para prova futura

Nas versões anteriores do Mozilla Firefox, eles também usam pequenas otimizações de números inteiros, como a V8, com os 3 bits baixos usados ​​para armazenar o tipo (int, string, objeto ... etc.). Mas desde o JägerMonkey, eles seguiram outro caminho ( a nova representação de valor JavaScript da Mozilla , link de backup ). O valor agora é sempre armazenado em uma variável de precisão dupla de 64 bits. Quando o doubleé normalizado , pode ser usado diretamente nos cálculos. No entanto, se os 16 bits altos forem todos 1s, que indicam um NaN , os 32 bits baixos armazenarão o endereço (em um computador de 32 bits) no valor ou no valor diretamente, os 16 bits restantes serão usados para armazenar o tipo. Esta técnica é chamada NaN-boxingou freira. Também é usado no JavaScriptCore do WebKit de 64 bits e no SpiderMonkey da Mozilla, com o ponteiro sendo armazenado nos 48 bits baixos. Se o seu tipo de dados principal é de ponto flutuante, esta é a melhor solução e oferece desempenho muito bom.

Leia mais sobre as técnicas acima: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

phuclv
fonte