共用体

共用体は、Cプログラミング言語から継承された低レベルの機能で、複数のメンバーが同じメモリ領域を共有できるようにする。

共用体は構造体とよく似ているが、主な違いは次の通りだ。

構造体と同様に、共用体もメンバー関数を持つことができる。

以下の例は、32ビット環境と64ビット環境でコンパイルした場合、異なる結果になる。混乱を避けるため、この章の例をコンパイルする際には、-m32コンパイラスイッチを使用しよう。そうしないと、後で説明するアライメントの違いにより、私の結果と異なる結果になる可能性がある。

当然ながら、structオブジェクトは、すべてのメンバーを格納するために必要なサイズになる:

// 注釈: -m32コンパイラスイッチでコンパイルしよう
struct S {
    int i;
    double d;
}

// ...

    writeln(S.sizeof);
D

intは4バイト、doubleは8バイトであるため、structのサイズはそれらのサイズの合計になる:

12

一方、同じメンバーを持つunionのサイズは、最も大きなメンバーのサイズと同じだけになる:

union U {
    int i;
    double d;
}

// ...

    writeln(U.sizeof);
D

4バイトのintと8バイトのdoubleは同じ領域を共有する。その結果、unionの全体のサイズは、その最大のメンバーと同じになる:

8

共用体はメモリを節約する機能ではない。複数のデータを同じメモリ位置に格納することは不可能。共用体の目的は、異なる型のデータを異なるタイミングで同じ領域を使用することだ。一度に確実に使用できるメンバーは1つだけだ。ただし、この方法は異なるプラットフォームでは移植性がない場合があるが、unionのメンバーを使用して他のメンバーのフラグメントにアクセスすることは可能だ。

以下の例の1つは、typeidを利用して、現在有効なメンバー以外のメンバーへのアクセスを禁止している。

以下の図は、上記のunionの8バイトがメンバー間で共有される方法を示している:

       0      1      2      3      4      5      6      7
───┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬───
   │<───  4 bytes for int  ───>                            │
   │<───────────────  8 bytes for double  ────────────────>│
───┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───

8バイトすべてがdoubleメンバーに使用されるか、最初の4バイトがintメンバーに使用され、残りの4バイトは未使用となる。

共用体には、必要な数のメンバーを含めることができる。すべてのメンバーは、同じメモリ位置を共有する。

すべてのメンバーに同じメモリ位置が使用されることは、予想外の結果をもたらすことがある。例えば、unionオブジェクトをそのintメンバーで初期化し、そのdoubleメンバーにアクセスすると、次のような結果になる。

auto u = U(42);    // intメンバーを初期化する
writeln(u.d);      // doubleメンバーにアクセスする
D

intメンバーを値42で初期化すると、最初の4バイトだけが設定され、doubleメンバーに予期しない影響が出る。

2.07508e-322

マイクロプロセッサのエンディアンに応じて、4バイトはメモリ内で 0|0|0|42、42|0|0|0、またはその他の順序で配置される場合がある。そのため、doubleメンバーの値は、プラットフォームによって異なって表示される場合がある。

匿名共用体

匿名共用体は、ユーザー定義型のどのメンバーが同じ領域を共有するかを指定する。

struct S {
    int first;

    union {
        int second;
        int third;
    }
}

// ...

    writeln(S.sizeof);
D

Sの最後の2つのメンバーは同じ領域を共有する。したがって、structのサイズは、2つのintの合計となる:firstに必要な4バイトと、secondthirdで共有される4バイト:

8
他のメンバーの分解

共用体は、他の型の変数の個々のバイトにアクセスするために使うことができる。例えば、IPv4アドレスの4バイトに個別にアクセスするのが簡単になる。

IPv4アドレスの32ビット値と固定長配列は、unionの2つのメンバーとして定義できる。

union IpAddress {
    uint value;
    ubyte[4] bytes;
}

そのunionのメンバーは、以下の図のように同じメモリ領域を共有する:

        0          1          2          3
───┬──────────┬──────────┬──────────┬──────────┬───
   │ <────  32 bits of the IPv4 address  ────> │
   │ bytes[0] │ bytes[1] │ bytes[2] │ bytes[3] │
───┴──────────┴──────────┴──────────┴──────────┴───

例えば、このunionのオブジェクトが0xc0a80102(ドット形式の192.168.1.2に対応する値)で初期化されると、bytes配列の要素は自動的に4つのオクテットの値になる。

import std.stdio;

void main() {
    auto address = IpAddress(0xc0a80102);
    writeln(address.bytes);
}

リトルエンディアンシステムで実行すると、オクテットはドット表記の逆の順序で表示される。

[2, 1, 168, 192]

オクテットの逆の順序は、unionの異なるメンバーにアクセスすると、予測できない結果が生じるもう1つの例だ。これは、unionの動作は、そのunionが1つのメンバーを通じてのみ使用されている場合にのみ保証されるためだ。unionが初期化に使用されたメンバー以外のメンバーの値については、何の保証もない。

この章とは直接関係はないが、core.bitopモジュールにあるbswapは、エンディアンの問題に対処するのに役立つ。bswapは、パラメータのバイトをスワップしてからそのパラメータを返す。また、std.systemモジュールにあるendianの値を利用すると、前のIPv4アドレスのオクテットを、バイトをスワップしてから期待される順序で出力することができる。

import std.system;
import core.bitop;

// ...

    if (endian == Endian.littleEndian) {
        address.value = bswap(address.value);
    }
D

出力:

[192, 168, 1, 2]

IpAddress型は単純な例として考えてほしい。一般的には、単純なプログラム以外の場合は、専用のネットワークモジュールを検討したほうがいいだろう。

通信プロトコル

TCP/IPなどの一部のプロトコルでは、プロトコルパケットの特定の部分の意味は、同じパケット内の特定の値によって決まる。通常、連続するバイトの意味を決定するのは、パケットのヘッダーにあるフィールドだ。共用体は、このようなプロトコルパケットを表現するために使うことができる。

以下の設計は、2種類のプロトコルパケットを表している。

struct Host {
    // ...
}

struct ProtocolA {
    // ...
}

struct ProtocolB {
    // ...
}

enum ProtocolType { A, B }

struct NetworkPacket {
    Host source;
    Host destination;
    ProtocolType type;

    union {
        ProtocolA aParts;
        ProtocolB bParts;
    }

    ubyte[] payload;
}
D

上記のstructは、typeメンバーを使用して、unionaPartsまたはbPartsのどちらを使用するかを決定できる。

識別された共用体

共用体は、通常のunionに型安全性を追加したデータ構造だ。unionとは異なり、現在有効なメンバー以外のメンバーにはアクセスできない。

以下は、intdoubleの2つの型のみをサポートする、単純な共用体型だ。データを格納するunionに加えて、2つの メンバーのうちどちらが有効であるかを識別するための TypeInfoメンバーを保持して、2つのunionメンバーのうちどちらが有効かを判断する。

import std.stdio;
import std.exception;

struct Discriminated {
private:

    TypeInfo validType_;

    union {
        int i_;
        double d_;
    }

public:

    this(int value) {
        // これは、以下のプロパティ関数の呼び出しである:
        i = value;
    }

    // 'int'データのセッター
    void i(int value) {
        i_ = value;
        validType_ = typeid(int);
    }

    // 'int'データのゲッター
    int i() const {
        enforce(validType_ == typeid(int),
                "The data is not an 'int'.");
        return i_;
    }

    this(double value) {
        // これは、以下のプロパティ関数の呼び出しである:
        d = value;
    }

    // 'double'データのセッター
    void d(double value) {
        d_ = value;
        validType_ = typeid(double);
    }

    // 'double'データのゲッター
    double d() const {
        enforce(validType_ == typeid(double),
                "The data is not a 'double'." );
        return d_;
    }

    // 有効なデータの型を識別する
    const(TypeInfo) type() const {
        return validType_;
    }
}

unittest {
    // 'int'データから始めましょう
    auto var = Discriminated(42);

    // 型は'int'として報告されるはず
    assert(var.type == typeid(int));

    // 'int'ゲッターは動作するはず
    assert(var.i == 42);

    // 'double'ゲッターは失敗するはず
    assertThrown(var.d);

    // 'int'を'double'データに置き換えよう
    var.d = 1.5;

    // 型は'double'として報告されるはず
    assert(var.type == typeid(double));

    // これで'double'ゲッターは動作するはず...
    assert(var.d == 1.5);

    // ...そして'int'ゲッターは失敗するはず
    assertThrown(var.i);
}

これは単なる例だ。プログラムでは、std.variantモジュールにあるAlgebraicおよびVariantを使用することを検討しよう。さらに、このコードでは、テンプレートやミックスインなどのDの他の機能を利用して、コードの重複を減らすことができる。

格納されるデータに関係なく、Discriminated型は1つだけだ。(別のテンプレートソリューションでは、データ型をテンプレートパラメータとして受け取ることもできる。その場合、テンプレートの各インスタンスは異なる型になる。)そのため、Discriminatedオブジェクトの配列を作成することができ、要素が異なる型のコレクションを効果的に実現できる。ただし、ユーザーは、アクセスする前に有効なメンバーを知っている必要がある。例えば、次の関数は、Discriminatedtypeプロパティを使用して、有効なデータの実際の型を決定する。

void main() {
    Discriminated[] arr = [ Discriminated(1),
                            Discriminated(2.5) ];

    foreach (value; arr) {
        if (value.type == typeid(int)) {
            writeln("Working with an 'int'  : ", value.i);

        } else if (value.type == typeid(double))  {
            writeln("Working with a 'double': ", value.d);

        } else {
            assert(0);
        }
    }
}
D
'int'の取り扱い1
'double'の取り扱い2.5