Відповіді:
Ви можете зробити елементи масиву розміченого об'єднання, інакше позначений накидной .
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 для таких речей, як Події.
Приклад у відповіді Бармара дає назву val
внутрішньому союзу. У прикладі у відповіді Сп. Використовується анонімне об'єднання, щоб уникнути необхідності вказувати .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
Інша річ, що це дозволяє - це форма успадкування. Редагувати: ця частина не є стандартною C, але використовує розширення 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-розрядний тип), а два найменш значущі біти повинні бути 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-бітній версії двигуна V8 Chrome , де вона перевіряє найменш значущий біт адреси, щоб побачити, чи це вказівник на інший об’єкт (наприклад, подвійні, великі цілі числа, рядок чи якийсь об’єкт) або 31 -бітове підписане значення (називається smi
- мале ціле число ). Якщо це an 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 використовували мічені адреси під назвою Handles для зберігання посилань на об'єкти даних. Високі біти адреси вказували на те, чи був об'єкт даних заблокованим, очищеним та / або походить відповідно з файлу ресурсів. Це спричинило проблеми з сумісністю, коли адресація 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-боксабо монахині. Він також використовується в 64-розрядних WebKit JavaScriptCore і Mozilla's SpiderMonkey, при цьому вказівник зберігається з низькими 48 бітами. Якщо ваш основний тип даних є плаваючою комою, це найкраще рішення і забезпечує дуже хороші показники.
Детальніше про вищезазначені методи читайте: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations