strict aliasing rules, type punning解説 その2(union)

前回述べた strict aliasing rules の集成体とunionのルールについては分かりずらいです。自身も誤解していた部分があったので追加で解説を試みます。

このルールに該当するコードは以下のようものだと思っていましたが違うようです。

struct s {
 int i;
};
static_assert(sizeof(s) == sizeof(int), "");

alignas(max_align_t) int a=0; //int型としてアクセス
s* p = (s*)(void*)&a;
s b = *p; //int型のオブジェクトaをs型としてアクセス OK? 
// (sはメンバにint型を含む集成体だから)

// しかし以下のような例を考えると
struct t {
int i;
};
static_assert(sizeof(t) == sizeof(int), "");
int f(s* p, t* q) {
 p->i = q->i + 5;
 return q->i;
}

int main() {
  alignas(std::max_align_t) int a=0;
  return f((s*)(void*)&a, (t*)(void*)&a); 
}
// 関数f の g++ -O3 のアセンブリは
//	movl	(%rdx), %eax
//	leal	5(%rax), %edx
//	movl	%edx, (%rcx)
//	ret   //rdxをリロードせずeaxをそのまま返している。
// gccとclangはこの例においてはエイリアシングを考慮していない。

gccとclangの仕様に合わせて解釈するのであればこのルールは以下のようにsとintの間にエイリアシングを考慮することを指していると考えられます。それぞれが元の型と同じ型でアクセスしているだけであり当然すぎる気もしますが最適化をするという趣旨から考えるとなるべくaliasingを考慮しないのは一番納得できる解釈です。現実のコンパイラ仕様からみるとプログラマは構造体Aの型Tのメンバと構造体Bの型Tのメンバの間にエイリアスが考慮されないとしてコーディングする必要があります。たとえ同じ型Tであっても別々の構造体のメンバであることがわかる状況であればエイリアスは考慮されないのです!

s a{};
func(&a, &a.i);

void func(s* x, int* y) {
 *x = s(); //yの指す先にも変更が及ぶ可能性が考慮される。
 int b = *y; //OK sとintのエイリアスを考慮。
}
//gccとclangの挙動をまとめると
struct s { int i; };
struct t { int i; };
void func(s* x, int* y); //sとintのエイリアシングは考慮される
void func(s* x, t* y); //sとtのエイリアシングは考慮されず、
//sのintメンバとtのintメンバはint型同士であってもエイリアシングは考慮されない。

unionについても集成体と同じくルールが及ぶのは以下のような場合であると考えられます。

union u {
 int i;
};
u a;
func(&a, &a.i);
void func(u* x, int* y) {
 *y = 0; //xの指す先にも変更が及ぶ可能性が考慮される。
 u b = *x; //OK uとintのエイリアスを考慮。 (uはメンバにint型を含むunionだから)
 //規格ではOKだと思うがgccではunionのメンバをポインタ経由で触っているの
 //でundefined behaviorとなる。
}


ではunionのメンバについてはどうでしょうか
前回紹介した通り以下の例は未定義動作となります(gccもしくはC99以降ではOKですが)。

union u {
 int i;
 float f;
};

u a;
a.i = 0; //int型としてアクセス
float v = a.f; //上記のint型のオブジェクトをfloat型としてアクセス undefined behavior

では以下の例はどうでしょうか

union u {
 int i;
 float f;
};

u a;
a.i = 0; //int型としてアクセス
a.f = 0; //上記のint型のオブジェクトをfloat型としてアクセス?
// ん?intとfloatの間にエイリアシングは考慮されないのでは?

かなり怪しく思えますが、この例の場合は a.f = 0; の時に以前のint型のオブジェクトが破棄され新しくfloat型のオブジェクトを構築したと考えればstrict aliasing rulesに矛盾しないように思えます。つまり一般的にいうvariant型のようにある時点で単一のオブジェクトが格納されていて、その寿命期間中にそのオブジェクトの型としてしか扱わないのであれば問題ないように思えるし、規格にもそれを前提としているような例がみられます。
ただし、両方のメンバのポインタを取る例を考えると途端に雲行きが怪しくなります。以下のような例ではコンパイラはunionのメンバを破棄、構築したとは考えないでしょう。

u a;
func2(&a.i, &a.f);

void func2(int* x, float* y) {
 *x = 0;
 *y = 0;
 //まさかint型とfloat型が同じアドレスを指しているとはコンパイラは夢にも思わないだろう。
 //そしてエイリアシングが存在しない前提で最適化を行ってしまい
 //  _人人人人人人人人人人_
 //  > 突然の未定義動作 <
 //   ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
}


unionのメンバのstructがcommon initial sequenceを持つ場合
C99では特別な保証として以下のようなことが有効であるとされています(ただしunionの完全型が見えている場所に限られる)。

union {
 struct { int m; } s1;
 struct { int n; } s2;
} u;
u.s1.m = 1;
int v = u.s2.n;

しかしこのルールはC++ではNoteになっています。またC99にあるunionの完全型が見えている場所という条件は付いていませんがそもそもNoteなのであまり当てにはできません。


さて所変わってC++11からはコンストラクタ、デストラクタを持つようなクラスもunionのメンバとして使用できるようになりました。このメンバを使うには明示的なデストラクタの呼び出しと配置newが必要になります。配置newを使用するにはunionのメンバのポインタを取る必要があるのですがこれはgccが未定義動作としています。一方で新しいgccはこのC++11のunionをサポートしたと謳っているのでこの配置newを使う場合に限ってはunionのメンバのポインタを取ることは問題ないと考えられます。

次回一般的なalias問題