共用体

共用体は、プログラミング言語におけるデータ型の一つで、同じメモリ領域を複数の型が共有する構造である。

例として、ある入力が数字の場合は数値として、そうでない場合は文字列のまま保持したいという場合を考える。この場合、数値用と文字列用の領域をそれぞれ用意するのが一つの解法だが、入力は数値か文字列のどちらか一方なので、片方しか使われず無駄が出る。そこで代わりに、格納用の領域を一つだけ用意して、これを数値である、文字列であると場合により解釈し分けることで領域の無駄が抑えられる。この「格納用の領域」こそが共用体である。

共用体から意味のある値を取り出すためには、中身のデータそのものに加えて「今、何の型のデータが入っているか」という情報が必要となる。タグを付加情報として持ち、常に正しい型でデータを得られるように設計された共用体を特にタグ付き共用体あるいはバリアント型という。

一方で、タグの付いていない共用体の場合は、正しい型でアクセスすることは利用者側の責任である。利用者は何らかの方法で共用体に今何が入っているかを管理しなければならない。誤った型でアクセスした場合、例えば数値の入った共用体から文字列を取り出そうとして得られた値は大抵は無意味か不正なものとなる。ただし、敢えて格納時と異なる型で値にアクセスすることで、一つのバイト列に対して複数の型で解釈するテクニックもある。例としては、ある整数型の値が格納された共用体に、より小さな整数型が格納されているものとしてアクセスすることで、元々の長い整数の上位／下位バイト部分を取り出すことができる。このテクニックは実際にはエンディアンなど環境に強く依存し、移植性は低い。

1. C言語
------

C言語は共用体をサポートしている。Cの共用体は全てのメンバのオフセットが0である構造体であり、宣言に予約語structではなく共用体を意味するunionを使うことを除いて構造体と全く同じ構文で宣言・定義される。またメンバへのアクセスも構造体と同様に.演算子あるいは->演算子で行える。共用体全体のサイズは少なくともメンバの中で最大のものを格納できる大きさに決められる。

共用体の初期化は、先頭で宣言したメンバの型で行わなければならない。

構造体を含む共用体については配置について特別な規定があり、共用体のメンバとして、先頭のメンバの型が同じである構造体を複数持つ場合は、それらの構造体の先頭メンバに限り、正確に重なり合うことが保証される。つまり、先頭メンバは別の構造体の先頭メンバの名前でも正しく参照できる。この仕様は似たような構成の構造体を集めた共用体で、先頭メンバをタグ情報として使うために役立つ。

include
include /* 共用体 MyUnion1 を定義 */ union MyUnion1 { double x; int y; char z[10]; /* double, int, char[10]のいずれかを格納できる */ };

/* 共用体 MyUnion2 を定義：構造体の共用体 */ union MyUnion2 { struct Foo { int ifoo; double dfoo; } foo; /* 構造体Fooとメンバfooの定義 */ struct Bar { int ibar; void *pbar; } bar; /* 構造体Barとメンバbarの定義 */ }; /* FooとBarの先頭メンバは同じ型 */

/* 共用体を使ってみる */ int main { /* 共用体変数の宣言と初期化 */ /* 先頭メンバの型で初期化しなければならない */ union MyUnion1 u1 = { 100.0 }; union MyUnion2 u2;

ifdef __cplusplus/* C++ では、入れ子になった型はスコープ解決演算子で名前修飾する必要がある */ MyUnion2::Foo f = { 3, 0.14 };

elsestruct Foo f = { 3, 0.14 };

endifu1.y = 42; /* int型の値を書き込む */ strcpy; /* charの配列を書き込む */ /* u1.y += 100; */ /* 不正: charの配列が入ったu1をint型として扱っている */

u2.foo = f; /* Foo構造体を書き込む */ u2.bar.ibar = 9; /* OK: Barの先頭メンバとしてアクセスしても良い */ /* u2.bar.pbar = NULL; */ /* これはダメ: 今u2に入っているのはあくまでFoo型 */

return 0; }<WtXmlEndTag />

C99までは無名の構造体および共用体が許可されていなかったが、C11では許可されるようになった。

include enum VariantType { VariantTypePointer, VariantTypeInt, VariantTypeDouble, };

struct Variant { enum VariantType type; union { /* 無名の共用体 */ void* p; int i; double d; }; };

int main { struct Variant v1; v1.type = VariantTypePointer; v1.p = NULL; v1.type = VariantTypeInt; v1.i = 999; v1.type = VariantTypeDouble; v1.d = 0.5; return 0; }<WtXmlEndTag />

2. C++
------

C++の共用体はクラスの一種で、Cの共用体の機能に加え、メンバ関数を持てるなど機能が追加されている。ただし普通のクラスに比べ以下のような制約が加わっている。

継承の機能がない。
基底クラスを持つことができない。
共用体から派生することはできない。
仮想関数を持てない。
静的メンバを持てない。
PODでないクラスのオブジェクト型のメンバを持てない。
参照型のメンバを持てない。これらの制約の一部はC++11で撤廃された。

また、C++では共用体タグ名を省略して定義することで、スコープを形成しない無名の共用体を作れるようになった。ただし、無名の構造体は許可されていない。

先の制約のために、標準ライブラリのものも含めてほとんどのクラスは共用体に格納できない。これは共用体がタグ情報を持たないために、コンストラクタやデストラクタを正しく実行するコードを自動生成できないからである。そのため、C++で共用体が積極的に利用されることは少ない。

なお、共用体に類似したC++の機能にreinterpret_castがあり、バイト列再解釈の用途にはこちらが用いられることが一般的である。

include
include // 共用体Uを定義 union U { private: double x; int y; //std::string z; // 不正: PODでないクラスはメンバにできない std::string *z; // ポインタは可 public: // コンストラクタ、デストラクタおよびメンバ関数を持てる explicit U : x { std::cout << "U" << std::endl; } explicit U : y { std::cout << "U" << std::endl; } ~U { std::cout << "~U" << std::endl; } double getDouble const { return x; } int getInt const { return y; } };

int main { U u1, u2; // コンストラクタによる共用体の構築

std::cout << u1.getDouble << std::endl; // メンバ関数呼び出し std::cout << u2.getInt << std::endl; //std::cout << u2.getDouble << std::endl; // ※何が起こるかわからない

// 無名共用体 union { int i; char c; }; i = 777; // メンバは外から見える c = 'X';

std::cout << c << std::endl; //std::cout << i << std::endl; // ※何が起こるかわからない

return 0; }<WtXmlEndTag />

3. C#
-----

C#では、共用体専用の構文は存在しない。 ただし、System.Runtime.InteropServices.StructLayoutAttributeで明示的なレイアウトを指定した構造体では、共用体同様の動作を指定することが可能である。

以下に例を示す。

using System; using System.Runtime.InteropServices;

[StructLayout] public struct Union { // メモリレイアウトのオフセットを指定する [FieldOffset] public byte B0; [FieldOffset] public byte B1; [FieldOffset] public byte B2; [FieldOffset] public byte B3; // メモリレイアウトのオフセットを B0, B1, B2, B3 と同じ領域に重ねる [FieldOffset] public int N; }

class Program { static void Main { var u = new Union; // N の領域に値を書き込む u.N = 0x12345678; // B0, B1, B2, B3 の領域より値を読み出す Console.WriteLine); // ⇒ 12345678 Console.WriteLine); // ⇒ リトルエンディアンでは 78 Console.WriteLine); // ⇒ リトルエンディアンでは 56 Console.WriteLine); // ⇒ リトルエンディアンでは 34 Console.WriteLine); // ⇒ リトルエンディアンでは 12 } }<WtXmlEndTag />

4. 脚注
-----

5. 出典
-----

6. 関連項目
-------

構造体