導入
第7章で union は「複数のメンバが同じメモリ領域を共有する型」だと学びました。この性質を活かすと、「今この変数にはどの型の値が入っているか」を別の変数(タグ)で管理するタグ付きunionが作れます。異なる型の値を、必要なだけのメモリで1つの変数として扱える、実務でもよく使われるテクニックです。
説明
タグ付きunion ― 「今どの型か」をenumで管理する
#include <stdio.h>
enum ValueType { TYPE_INT, TYPE_DOUBLE };
typedef struct {
enum ValueType type; /* 今どちらの型が入っているかを示すタグ */
union {
int i;
double d;
} value;
} Variant;
void printVariant(Variant v) {
if (v.type == TYPE_INT) {
printf("int:%d\n", v.value.i);
} else {
printf("double:%.1f\n", v.value.d);
}
}
int main(void) {
Variant a;
a.type = TYPE_INT;
a.value.i = 42;
Variant b;
b.type = TYPE_DOUBLE;
b.value.d = 3.5;
printVariant(a);
printVariant(b);
return 0;
}
Variant は int と double のどちらか一方だけを、必要なメモリ(大きい方のサイズ)で保持できます。type フィールドが「今 value の中身をどう読めばよいか」の目印になっており、これを見てから正しいメンバにアクセスするのがタグ付きunionの使い方です(type を見ずに間違ったメンバを読むと、意味のないビット列を別の型として解釈することになります)。
アライメント ― メモリ上の並び方
構造体のメンバは、メモリ上で単純に隙間なく並ぶとは限りません。多くの環境では、int は4バイト境界、double は8バイト境界のように、型ごとに「都合のよい開始位置(アライメント)」があり、コンパイラはそこに合わせるためにパディング(隙間)を挟むことがあります。
#include <stdio.h>
struct Example {
char c;
int i;
};
int main(void) {
printf("sizeof(char)=%zu sizeof(int)=%zu\n", sizeof(char), sizeof(int));
printf("sizeof(struct Example)=%zu\n", sizeof(struct Example));
printf("_Alignof(int)=%zu\n", (size_t)_Alignof(int));
return 0;
}
struct Example は char(1バイト)と int(4バイト)を持つので、単純に足すと5バイトに思えますが、実際には int のアライメントに合わせてパディングが入り、それより大きい値になることがあります(値は環境によって変わります)。C11の _Alignof 演算子を使うと、ある型が要求するアライメントの値を調べられます。
assert ― 前提条件を実行時にチェックする
<assert.h> の assert(条件) は、「ここではこの条件が必ず成り立っているはずだ」という前提をコードに書いておく道具です。条件が真であれば何も起きませんが、もし偽になったら、プログラムはその場でエラーメッセージを出して異常終了します。
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); /* b が0でないことを前提とする */
return a / b;
}
int main(void) {
printf("%d\n", divide(10, 2));
return 0;
}
assert はバグを未然に発見するための道具であって、if のように失敗を優雅に処理するものではありません(失敗したら即座に止まります)。開発中は assert をたくさん入れて前提条件の破れをすぐ見つけられるようにし、NDEBUG というマクロを定義してコンパイルすると、すべての assert が無効化されて本番のコードから取り除かれる、という使い分けもよくされます。
やってみよう
Variant の値を TYPE_INT/TYPE_DOUBLE それぞれで色々な値に変えて printVariant の結果を確かめましょう。divide(10, 0) のように b に 0 を渡すとどうなるか(assert が失敗して異常終了します)も、余裕があれば試してみてください。
演習
上の Variant・printVariant をそのまま使い、TYPE_INT で値 100 を持つ変数と、TYPE_DOUBLE で値 2.5 を持つ変数を1つずつ作り、この順に printVariant で表示してください(正しく書けると int:100 と double:2.5 の2行になります)。
ヒント1を見る
Variant 変数を作ってから 変数名.type = TYPE_INT;、変数名.value.i = 100; の順に値を入れます。
ヒント2を見る
もう1つの変数は type = TYPE_DOUBLE;、value.d = 2.5; とし、printVariant を2回呼びます。