Respostas:
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 type
membro é usado para manter a escolha de qual membro do union
deve ser usado para cada elemento da matriz. Portanto, se você deseja armazenar um int
no 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 switch
declaraçã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 type
membro sempre corresponda ao último valor armazenado no union
.
Use uma união:
union {
int ival;
float fval;
void *pval;
} array[10];
Você precisará acompanhar o tipo de cada elemento.
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.
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 .tag
inicializador 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
.int_.val
não alias a mesma área porque o compilador sabe que .val
está em um deslocamento maior que .tag
. Você tem um link para uma discussão mais aprofundada sobre esse suposto problema?
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 enum
valor.
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 int
devem 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 long
e 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.
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