Ответы:
Вы можете сделать элементы массива распознаваемым объединением, или теговым объединением .
struct {
enum { is_int, is_float, is_char } type;
union {
int ival;
float fval;
char cval;
} val;
} my_array[10];
Элемент type
используется для хранения выбора элемента, который union
должен использоваться для каждого элемента массива. Так что если вы хотите сохранить int
в первом элементе, вы должны сделать:
my_array[0].type = is_int;
my_array[0].val.ival = 3;
Когда вы хотите получить доступ к элементу массива, вы должны сначала проверить тип, а затем использовать соответствующий член объединения. switch
Заявление полезно:
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
}
Программист должен убедиться, что type
элемент всегда соответствует последнему значению, сохраненному в union
.
Используйте союз:
union {
int ival;
float fval;
void *pval;
} array[10];
Вы должны будете отслеживать тип каждого элемента.
Элементы массива должны иметь одинаковый размер, поэтому это невозможно. Вы можете обойти это, создав тип варианта :
#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;
}
Размер элемента объединения равен размеру самого большого элемента 4.
Существует другой стиль определения объединения тегов (с любым именем), который IMO делает его более приятным для использования , удаляя внутреннее объединение. Этот стиль используется в X Window System для таких вещей, как События.
Пример в ответе Бармара дает название val
внутреннему союзу. Пример в ответе Sp. Использует анонимное объединение, чтобы не указывать .val.
каждый раз, когда вы обращаетесь к варианту записи. К сожалению, «анонимные» внутренние структуры и союзы недоступны в C89 или C99. Это расширение компилятора, и, следовательно, изначально не переносимое.
Лучший способ ИМО - инвертировать все определение. Сделайте каждый тип данных своей собственной структурой и поместите тег (спецификатор типа) в каждую структуру.
typedef struct {
int tag;
int val;
} integer;
typedef struct {
int tag;
float val;
} real;
Затем вы заключаете их в союз высшего уровня.
typedef union {
int tag;
integer int_;
real real_;
} record;
enum types { INVALID, INT, REAL };
Теперь может показаться, что мы повторяем себя, и мы это делаем . Но учтите, что это определение, вероятно, будет выделено в один файл. Но мы устранили шум указания промежуточного звена, .val.
прежде чем вы получите данные.
record i;
i.tag = INT;
i.int_.val = 12;
record r;
r.tag = REAL;
r.real_.val = 57.0;
Вместо этого это идет в конце, где это менее неприятно. : D
Другая вещь, которую это позволяет, является формой наследования. Редактировать: эта часть не стандартная Си, но использует расширение 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;
Повышение и понижение.
Редактировать: один момент, о котором нужно знать, - это если вы создаете один из них с помощью инициализированных C99 инициализаторов Все инициализаторы должны быть через одного и того же члена объединения.
record problem = { .tag = INT, .int_.val = 3 };
problem.tag; // may not be initialized
.tag
Инициализатор может быть проигнорирован оптимизирующего компилятора, потому что .int_
инициализатор , который следует псевдонимами той же области данных. Хотя мы знаем макет (!), И все должно быть в порядке. Нет, это не так. Вместо этого используйте «внутренний» тег (он накладывается на внешний тег, как мы и хотим, но не путает компилятор).
record not_a_problem = { .int_.tag = INT, .int_.val = 3 };
not_a_problem.tag; // == INT
.int_.val
не псевдоним той же области, потому что компилятор знает, что .val
с большим смещением, чем .tag
. У вас есть ссылка для дальнейшего обсуждения этой предполагаемой проблемы?
Вы можете сделать void *
массив с отдельным массивом size_t.
Но вы потеряете тип информации.
Если вам нужно каким-то образом сохранить тип информации, сохраните третий массив int (где int - это перечисляемое значение). Затем закодируйте функцию, которая приводится в зависимости от enum
значения.
Союз - это стандартный путь. Но у вас есть и другие решения. Одним из них является теговый указатель , который включает в себя хранение дополнительной информации в «свободных» битах указателя.
В зависимости от архитектуры вы можете использовать младшие или старшие биты, но самый безопасный и переносимый способ - использовать неиспользуемые младшие биты , используя преимущество выровненной памяти. Например, в 32-разрядных и 64-разрядных системах указатели int
должны быть кратны 4 (при условии, int
что это 32-разрядный тип), а 2 младших значащих бита должны быть 0, поэтому их можно использовать для хранения типа ваших значений. , Конечно, вам нужно очистить биты тега перед разыменованием указателя. Например, если ваш тип данных ограничен 4 различными типами, вы можете использовать его, как показано ниже
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;
}
Если вы можете убедиться, что данные выровнены по 8 байтов (например, для указателей в 64-битных системах или long long
и uint64_t
...), у вас будет еще один бит для тега.
Это имеет один недостаток: вам понадобится больше памяти, если данные не были сохранены в другой переменной. Поэтому в случае, если тип и диапазон ваших данных ограничен, вы можете хранить значения непосредственно в указателе. Этот метод использовался в 32-битной версии движка Chrome V8 , где он проверяет младший значащий бит адреса, чтобы определить, является ли это указателем на другой объект (например, double, большие целые числа, строку или некоторый объект) или 31 -значное знаковое значение (называемое smi
- маленькое целое ). Если это int
, Chrome просто выполняет арифметическое смещение вправо на 1 бит, чтобы получить значение, в противном случае указатель разыменовывается.
В большинстве современных 64-разрядных систем виртуальное адресное пространство все еще намного уже 64-разрядных, поэтому старшие старшие биты также можно использовать в качестве тегов . В зависимости от архитектуры у вас есть разные способы использовать их в качестве тегов. ARM , 68k и многие другие могут быть сконфигурированы так, чтобы игнорировать верхние биты , что позволяет вам свободно использовать их, не беспокоясь о segfault или чем-либо еще. Из приведенной выше статьи в Википедии:
Важным примером использования теговых указателей является среда выполнения Objective C на iOS 7 на ARM64, особенно используемая на iPhone 5S. В iOS 7 виртуальные адреса имеют размер 33 бита (с байтовым выравниванием), поэтому адреса с выравниванием по словам используют только 30 бит (3 младших бита равны 0), оставляя 34 бита для тегов. Указатели класса Objective-C выровнены по словам, а поля тегов используются для многих целей, таких как хранение счетчика ссылок и наличие в объекте деструктора.
Ранние версии MacOS использовали теговые адреса, называемые дескрипторами, для хранения ссылок на объекты данных. Старшие биты адреса указывали, был ли объект данных заблокирован, очищен и / или создан из файла ресурсов, соответственно. Это вызвало проблемы совместимости, когда адресация MacOS увеличилась с 24 бит до 32 бит в System 7.
На x86_64 вы все равно можете осторожно использовать старшие биты в качестве тегов . Конечно, вам не нужно использовать все эти 16 битов, и вы можете оставить некоторые биты для будущего
В предыдущих версиях Mozilla Firefox они также использовали небольшие целочисленные оптимизации, такие как V8, с 3 младшими битами, используемыми для хранения типа (int, string, object ... и т. Д.). Но начиная с JägerMonkey они пошли другим путем ( Новое представление значений JavaScript в Mozilla , ссылка для резервного копирования ). Значение теперь всегда сохраняется в 64-битной переменной двойной точности. Когда double
это нормализованное один, он может быть непосредственно использован в расчетах. Однако, если все старшие 16 битов равны 1, это означает NaN , младшие 32 бита сохранят адрес (в 32-битном компьютере) непосредственно в значении или значении, остальные 16 бит будут использованы хранить тип. Эта техника называется NaN-боксомили монахиня-бокс. Он также используется в JavaScript-ядре 64-битного WebKit и SpiderMonkey от Mozilla, причем указатель хранится в младших 48 битах. Если ваш основной тип данных с плавающей запятой, это лучшее решение и обеспечивает очень хорошую производительность.
Узнайте больше о вышеупомянутых методах: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations