回答:
配列要素を区別された共用体、別名タグ付き共用体にすることができます。
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でイベントなどに使用されるスタイルです。
Barmarの回答の例でval
は、内部組合に名前を付けています。Sp。の回答の例で.val.
は、バリアントレコードにアクセスするたびに指定する必要がないように、匿名の共用体を使用しています。残念ながら、「匿名の」内部構造体と共用体は、C89またはC99では使用できません。これはコンパイラの拡張機能であるため、本質的に移植できません。
IMOのより良い方法は、定義全体を反転させることです。各データ型を独自の構造体にし、タグ(型指定子)を各構造体に挿入します。
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
これが許可するもう1つのことは、継承の形式です。編集:この部分は標準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;
アップキャスティングとダウンキャスティング。
編集:気をつけなければならないことの1つは、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の3番目の配列(intは列挙値)を保持しenum
ます。次に、値に応じてキャストする関数をコーディングします。
Unionは標準的な方法です。しかし、他のソリューションもあります。それらの1つはタグ付きポインターです。これには、ポインターの「空き」ビットにさらに多くの情報を格納することが含まれます。
アーキテクチャに応じて、下位ビットまたは上位ビットを使用できますが、最も安全で移植性の高い方法は、アライメントされたメモリを利用して未使用の下位ビットを使用することです。たとえば、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
)、タグ用のビットがもう1つあります。
これには、データが他の場所の変数に格納されていない場合、より多くのメモリが必要になるという欠点があります。したがって、データのタイプと範囲が制限されている場合は、値を直接ポインターに格納できます。この手法は、32ビットバージョンの ChromeのV8エンジンの、アドレスの最下位ビットをチェックして、それが別のオブジェクト(double、big integers、stringまたはなんらかのオブジェクトなど)へのポインターか、または31 -bit符号付き値(-small smi
integerと呼ばれる)。である場合int
、Chromeは1ビットの算術右シフトを実行して値を取得します。それ以外の場合、ポインターは逆参照されます。
現在のほとんどの64ビットシステムでは、仮想アドレス空間は64ビットよりもはるかに狭いため、最上位ビットもタグとして使用できます。アーキテクチャに応じて、タグとして使用する方法が異なります。ARM、68k、およびその他の多くは、上位ビットを無視するように構成できるため、segfaultなどを気にすることなく自由に使用できます。上記のリンクされたウィキペディアの記事から:
タグ付きポインターの使用の重要な例は、特にiPhone 5Sで使用されるARM64上のiOS 7のObjective-Cランタイムです。iOS 7では、仮想アドレスは33ビット(バイト境界)であるため、ワード境界のアドレスは30ビット(最下位3ビットは0)のみを使用し、タグには34ビットを残します。Objective-Cクラスポインターはワード境界で整列されており、タグフィールドは、参照カウントの格納やオブジェクトにデストラクターがあるかどうかなど、さまざまな目的で使用されます。
MacOSの初期のバージョンでは、ハンドルと呼ばれるタグ付きアドレスを使用して、データオブジェクトへの参照を保存していました。アドレスの上位ビットは、データオブジェクトがロックされているか、パージ可能であるか、リソースファイルから発信されているかをそれぞれ示しています。これにより、MacOSアドレッシングがSystem 7で24ビットから32ビットに進んだときに互換性の問題が発生しました。
x86_64 では、注意して上位ビットをタグとして使用できます。もちろん、これらの16ビットをすべて使用する必要はなく、将来の証明のためにいくつかのビットを省略することができます
以前のバージョンのMozilla Firefoxでは、V8のような小さな整数の最適化も使用し、3つの下位ビットを使用して型(int、string、objectなど)を格納していました。しかし、JägerMonkey以来、彼らは別の道をたどりました(Mozillaの新しいJavaScript値表現、バックアップリンク)。値は常に64ビットの倍精度変数に格納されるようになりました。double
が正規化されている場合は、直接計算に使用できます。ただし、上位16ビットがすべて1の場合、NaNを示します。場合、下位32ビットはアドレス(32ビットコンピューターの場合)に値または値を直接格納し、残りの16ビットが使用されます。タイプを保存します。この手法はNaNボクシングと呼ばれますまたは修道女ボクシング。また、64ビットのWebKitのJavaScriptCoreとMozillaのSpiderMonkeyでも使用され、ポインタは下位48ビットに格納されます。メインデータタイプが浮動小数点の場合、これが最良のソリューションであり、非常に優れたパフォーマンスを提供します。
上記の手法の詳細については、https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementationsをご覧ください。