構造体のメモリー配置と多重継承

C/C++において構造体(クラス)内のメモリーの配置がどうなるかというのは案外重要な情報だ。
組み込み開発の話ではない(組み込みでも重要だけど)、バイナリデータの入出力に置いて便利だからだ。

たとえば1byte/2byte/4byte/4byteと並んでいるバイナリデータがあったとして、それを構造体

struct Data {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
};

に読み込もうとした場合を考えよう。
仕様にのっとって礼儀正しく書くとこうなる

void readData(const uint8_t * const in, Data &out) {
  out.a = in[0];
  out.b = in[1] | (in[2] << 8);
  out.c = in[3] | (in[4] << 8) | (in[5] << 16) | (in[6] << 24);
  out.d = in[7] | (in[8] << 8) | (in[9] << 16) | (in[10] << 24);
}

だが仮にアライメントにより読み書きの制限がなく、構造体内のメンバーのメモリー配置が宣言した順序に1byteの隙間もなく敷き詰められており、なおかつ余分なデータが含まれないと仮定できる場合、以下のように書くことができる。

void readData(const uint8_t * const in, Data &out) {
  out = *reinterpret_cast<const Data *>(in);
}

ちなみにこれはmemcpyでも同様の結果が得られる。
そして、x86/64ではアライメントによる読み書きの制限はなく、主要な開発環境であるVC++やgccには構造体に詰め物を挟まない設定が存在する。
よって、下のように書くことは現実に可能である。

あくまで自分の主観ではあるが、バイナリ入出力に置いてこのテクニックは基本であり、事実今まで多用し続けてきた。
しかし、最近あることと組み合わせるとこの機能をうまく使えないことが分かった。
その機能とは、”多重継承”である。

そもそも継承している時点でスーパークラスとサブクラスのメンバーの配置がどうなるかは仕様の範囲外なのだから、使えなくて当然だと思うかもしれないが
そういうことではなく、単純にメソッドのみを持つ所謂インターフェースクラスを継承した場合の話だ。

ちなみに、JIS X 3014上では
※引用ここから


9.0.1:
クラスのオブジェクトは、メンバおよび基底クラスのオブジェクトの列(空であってもよい。)から成る。

9.2.17:
C互換構造体オブジェクトを指すポインタは、reinterpret_castを使って適切に変換することによって、
先頭のメンバを指すポインタとなる(又はそのメンバがビットフィールドの場合は、その格納単位を指す)。
また、逆も正しい。

参考:
したがって、C互換構造体オブジェクト内に、
適切に境界調整するために名前なしの詰め物が置かれることがあるが、
先頭に置かれることはない。

9.0.4:
C互換構造体は、次のいずれももたない集成体クラスとする。
―非C互換構造体(又はその型の配列)である非静的データメンバー
―非C互換共用体(又はその型の配列)である非静的データメンバ
―参照型である非静的データメンバ
―利用者定義のコピー代入演算子
―利用者定義のデストラクタ

8.5.1.1:
次のいずれをも持たない配列またはクラスを、集成体と呼ぶ。
-利用者宣言のコンストラクタ
-非公開又は限定公開の非静的データメンバ
-基底クラス
-仮想関数

※引用ここまで

となっているため、多重継承している=基底クラスを持っている=集成体ではなくなるため、先頭のメンバーとのreinterpret_castでの変換自体保証されていないというのが仕様上の扱いである。
では実際のところどうなるのか、コードを交えていろいろ試して行こう。

とりあえず基本形

#include <cstdio>
#include <cstdint>

#ifdef _MSC_VER
#define __attribute__(attr) 
#endif

#pragma pack(push, 1)
struct Data {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));
#pragma pack(pop)

void readData(const uint8_t * const in, Data &out) {
  out = *reinterpret_cast<const Data *>(in);
}

int main() {
  const uint8_t in[] = {0x12, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12};
  Data out;
  readData(in, out);
  printf("sizeof: %d\n", sizeof(Data));
  printf("out.a: %08x\n", out.a);
  printf("out.b: %08x\n", out.b);
  printf("out.c: %08x\n", out.c);
  printf("out.d: %08x\n", out.d);
  return 0;
}

※以後変更点のみ記載

実行結果:
Microsoft Visual Studio 2010 Verion 10.0.40219.1 SP1Rel(以下VC++)、gcc4.6.3(以下gcc)共に

sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

となり、意図したとおりに動作していることが分かる。

一つ空の構造体を継承させてみる

struct Hoge {
};
struct Data : Hoge {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));
sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

VC++、gccともに実行結果に変化なし

さらに継承ツリーを伸ばしてみる


struct Fuga {
};
struct Hoge : Fuga {
};
struct Data : Hoge {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));
sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

VC++、gccともに実行結果に変化なし

多重継承にしてみる

struct Fuga {
};
struct Hoge {
};
struct Data : Hoge, Fuga {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));

VC++

sizeof: 12
out.a: 00000034
out.b: 00007812
out.c: 78123456
out.d: cc123456

gcc

sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

VC++だけサイズが1byte増え、範囲外アクセスを起こしてしまいました。
aの中身を見るに、メンバーaの前に余分な詰め物が1byteあるようです。

多重継承したものを継承してみる。

struct Fuga {
};
struct Hoge {
};
struct Haga : Hoge, Fuga {
};
struct Data : Haga {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));

VC++

sizeof: 12
out.a: 00000034
out.b: 00007812
out.c: 78123456
out.d: cc123456

gcc

sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

単純に多重継承したのと同じ結果、どうやら直接にしろ間接にしろ多重継承が混ざること自体がだめらしい。

さらに多重継承してみる。

struct Fuga {
};
struct Hoge {
};
struct Haga {
};
struct Data : Hoge, Fuga, Haga {
  uint8_t a;
  uint16_t b;
  uint32_t c;
  uint32_t d;
} __attribute__((packed));

VC++

sizeof: 13
out.a: 00000012
out.b: 00005678
out.c: 56781234
out.d: cccc1234

gcc

sizeof: 11
out.a: 00000012
out.b: 00001234
out.c: 12345678
out.d: 12345678

VC++がさらに悪化。
多重継承が一個増えるごとに謎の詰め物が一byteずつ追加されていく仕組みらしい。

結論:
VC++ではバイナリデーターからの一括読み込みなどに構造体を使用する場合、メソッドやコピー禁止クラスなどを多重継承させてはいけない。
gccなら問題なし。

この記事書く前にいろいろ試した段階ではgccでも壊れるパターンがあったはずなのだが思い出せなかった……orz