ISO/IEC 9899:1999(参照:6.5.2.3)を読んだときに、次のような例を見ました(強調鉱山):
以下は有効なフラグメントではありません(ユニオン型は関数
f
内では表示されないためです):struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 * p1, struct t2 * p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
テストしてもエラーや警告は見つかりませんでした。
私の質問は、なぜこのフラグメントが無効なのですか?
例では、事前に段落を説明しようとします1 (エンファシス鉱山):
6.5.2.3¶6
ユニオンの使用を簡素化するために、1つの特別な保証が行われます:ユニオンに共通の初期シーケンスを共有する複数の構造が含まれる場合(以下を参照)、ユニオンオブジェクトにこれらの構造のいずれかが現在含まれている場合それらのいずれかの共通の初期部分を検査して、完成した型の共用体の宣言が見える場所であればどこでも検査できます。対応するメンバーが1つ以上の初期メンバーのシーケンスに対して互換性のある型(およびビットフィールドの場合は同じ幅)を持っている場合、2つの構造は共通の初期シーケンスを共有します。
f
はg
の前に宣言され、さらに名前のないユニオン型はg
に対してローカルであるため、f
にユニオン型が表示されないことに疑問はありません。
この例では、u
の初期化方法を示していませんが、メンバーに最後に書き込まれたものがu.s2.m
であると仮定すると、共通の初期シーケンス保証なしでp1->m
を検査するため、関数の動作は未定義です効果。
関数呼び出しの前に最後に書き込まれたのがu.s1.m
である場合、p2->m
へのアクセスは未定義の動作ではなく、逆も同様です。
f
自体は無効ではないことに注意してください。これは完全に合理的な関数定義です。未定義の動作は、&u.s1
および&u.s2
を引数として渡すことに起因します。それが未定義の動作の原因です。
1 - n157 、C11標準ドラフトを引用しています。ただし、仕様は同じにする必要があり、段落を1つまたは2つ上下に移動するだけです。
動作中の厳密なエイリアスルールは次のとおりです.C(またはC++)コンパイラによって行われた前提の1つは、異なるタイプのオブジェクトへのポインターの逆参照が同じメモリロケーションを参照することはないということです(つまり、互いにエイリアスします)。
この機能
int f(struct t1* p1, struct t2* p2);
p1 != p2
が想定されているのは、それらが正式に異なる型を指しているからです。その結果、最適化者はp2->m = -p2->m;
がp1->m
に影響を与えないと想定する場合があります。最初にp1->m
の値をレジスタに読み取り、0未満の場合は0と比較し、p2->m = -p2->m;
を実行して、最後にレジスタ値を変更せずに返します。
ここのユニオンは、すべてのユニオンメンバーが同じアドレスを持っているため、バイナリレベルでp1 == p2
を作成する唯一の方法です。
もう一つの例:
struct t1 { int m; };
struct t2 { int m; };
int f(struct t1* p1, struct t2* p2)
{
if (p1->m < 0) p2->m = -p2->m;
return p1->m;
}
int g()
{
union {
struct t1 s1;
struct t2 s2;
} u;
u.s1.m = -1;
return f(&u.s1, &u.s2);
}
g
は何を返す必要がありますか? +1
は常識に従います(f
で-1を+1に変更します)。しかし、-O1
最適化を使用してgccのAssemblyを生成する場合
f:
cmp DWORD PTR [rdi], 0
js .L3
.L2:
mov eax, DWORD PTR [rdi]
ret
.L3:
neg DWORD PTR [rsi]
jmp .L2
g:
mov eax, 1
ret
これまでのところ、例外はすべてあります。しかし、-O2
で試してみると
f:
mov eax, DWORD PTR [rdi]
test eax, eax
js .L4
ret
.L4:
neg DWORD PTR [rsi]
ret
g:
mov eax, -1
ret
戻り値はハードコードされた-1
になりました
これは、先頭のf
がeax
レジスタ(p1->m
)のmov eax, DWORD PTR [rdi]
の値をキャッシュし、再読み取りしないの後p2->m = -p2->m;
(neg DWORD PTR [rsi]
)-eax
を変更せずに返します。
ここで使用されるユニオンは、ユニオンオブジェクトのすべての非静的データメンバが同じアドレスを持ちます。は結果&u.s1 == &u.s2
と同じです。
アセンブラーコードを理解していない人は、c/c ++で厳密なエイリアシングがfコードに与える影響を示すことができます:
int f(struct t1* p1, struct t2* p2)
{
int a = p1->m;
if (a < 0) p2->m = -p2->m;
return a;
}
コンパイラーはp1->m
値をローカル変数a
(実際にはレジスターに)でキャッシュし、p2->m = -p2->m;
変更p1->m
にもかかわらず、それを返します。しかし、コンパイラーは、p1
がp2
と重複しない別のメモリーを指していると仮定するため、p1
メモリーは影響を受けないと仮定します。
そのため、コンパイラと最適化レベルが異なると、同じソースコードが異なる値(-1または+1)を返す可能性があります。そのままで未定義の動作
共通初期シーケンスルールの主な目的の1つは、多くの類似した構造で機能を交換可能に動作させることです。構造体に作用する関数は、共通の初期シーケンスを共有する他の構造体の対応するメンバーを変更する可能性があるが、有用な最適化が損なわれる可能性があるとコンパイラーが想定する必要があります。
共通の初期シーケンスに依存するほとんどのコードは、いくつかの簡単に認識できるパターンを使用していますが、.
struct genericFoo {int size; short mode; };
struct fancyFoo {int size; short mode, biz, boz, baz; };
struct bigFoo {int size; short mode; char payload[5000]; };
union anyKindOfFoo {struct genericFoo genericFoo;
struct fancyFoo fancyFoo;
struct bigFoo bigFoo;};
...
if (readSharedMemberOfGenericFoo( myUnion->genericFoo ))
accessThingAsFancyFoo( myUnion->fancyFoo );
return readSharedMemberOfGenericFoo( myUnion->genericFoo );
異なるユニオンメンバーに作用する関数の呼び出し間のユニオンを再検討し、規格の著者は、呼び出された関数内のユニオン型の可視性が、関数へのアクセスがmode
のフィールドFancyFoo
は、mode
のフィールドgenericFoo
に影響する場合があります。その関数と同じコンパイル単位でreadSharedMemberOfGeneric
にアドレスが渡される可能性のあるすべてのタイプの構造を含む共用体を持つという要件により、共通初期シーケンスルールの有用性は低くなりますが、少なくとも上記のようないくつかのパターンを使用可能にします。
Gccとclangの作者は、関連する型が上記のようなコンストラクトに関与する可能性があることを示すものとして共用体宣言を扱うことは、最適化の非現実的な障害になると考え、標準ではそれらをサポートする必要がないと考えました他の方法で構築する場合、それらはまったくサポートしません。したがって、意味のある方法で共通初期シーケンスの保証を活用する必要があるコードの実際の要件は、ユニオン型宣言が表示されることを保証することではなく、clangとgccが-fno-strict-aliasing
国旗。また、実用的な場合は目に見える共用体宣言を含めることは問題ありませんが、gccおよびclangからの正しい動作を保証するために必要でも十分でもありません。