GCCでユニオンを使用して実行できることと実行できないことを理解するのに問題があります。私はそれについての質問(特に here および here )を読みましたが、それらはC++標準に焦点を当てています。C++標準と慣行(一般的に使用される)の間に不一致があると感じますコンパイラ)。
特に、私は最近、コンパイルフラグ-fstrict-aliasingについて読みながら、 GCCオンラインドキュメント で混乱する情報を見つけました。それは言う:
-fstrict-aliasing
コンパイラーが、コンパイルされる言語に適用可能な最も厳しいエイリアシング規則を想定できるようにします。 C(およびC++)の場合、これは式のタイプに基づいて最適化をアクティブにします。特に、あるタイプのオブジェクトは、タイプがほとんど同じでない限り、別のタイプのオブジェクトと同じアドレスに存在しないと想定されています。たとえば、
unsigned int
はint
のエイリアスを作成できますが、void*
またはdouble
のエイリアスは作成できません。文字タイプは他のタイプのエイリアスになる場合があります。次のようなコードに特に注意してください。union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }
最近書き込まれたものとは異なるユニオンメンバーから読み取る方法(「タイプパニング」と呼ばれます)は一般的です。 -fstrict-aliasingを使用した場合でも、メモリが共用体タイプを介してアクセスされる場合は、タイプパニングが許可されます。したがって、上記のコードは期待どおりに動作します。
これは私がこの例と私の疑問から理解したと思います:
1)エイリアスは、類似したタイプまたはcharの間でのみ機能します
1の結果):エイリアス-Wordが示すように、1つの値とそれにアクセスする2つのメンバー(つまり、同じバイト)がある場合です。
疑わしい:バイトのサイズが同じ場合、2つのタイプは似ていますか?そうでない場合、同様のタイプは何ですか?
1の結果非類似型の場合(これが何であれ)、エイリアスは機能しません。
2)型パンニングとは、書き込んだメンバーとは異なるメンバーを読み取る場合です。これは一般的であり、メモリが共用体タイプを介してアクセスされる限り、期待どおりに機能します。
疑い:は、タイプが類似しているタイプパンニングの特定のケースにエイリアスを設定していますか?
Unsigned intとdoubleは似ていないため、エイリアスが機能しないと表示されているので、混乱します。次に、この例では、intとdoubleの間のエイリアスであり、期待どおりに機能することを明確に示していますが、タイプパンニングと呼んでいます。しかし、それが書いていないメンバーから読むことは、私がエイリアシングの目的であると私が理解したことです(Wordが示唆しているように)。道に迷いました。
質問:エイリアスとタイプパンニングの違いを誰かが明確にできますか?2つのテクニックのどのような使い方がGCCで期待どおりに機能していますか?そしてコンパイラフラグは何をしますか?
エイリアスは、文字通り、その意味が異なります。2つの異なる式が同じオブジェクトを参照している場合です。タイプパンニングとは、タイプを「パンニング」することです。つまり、あるタイプのオブジェクトを別のタイプとして使用します。
正式には、型抜きは未定義の動作であり、いくつかの例外があります。不用意にビットをいじるとよく起こります
int mantissa(float f)
{
return (int&)f & 0x7FFFFF; // Accessing a float as if it's an int
}
例外は(簡略化された)
char
、unsigned char
、またはstd::byte
としてアクセスするこれは、厳密なエイリアシングルールとして知られています。コンパイラーは、異なるタイプの2つの式が同じオブジェクトを参照しない(上記の例外を除く)と、未定義の動作になるため、安全に想定できます。これにより、次のような最適化が容易になります
void transform(float* dst, const int* src, int n)
{
for(int i = 0; i < n; i++)
dst[i] = src[i]; // Can be unrolled and use vector instructions
// If dst and src alias the results would be wrong
}
Gccが言っていることは、ルールが少し緩和され、標準でそれが要求されていない場合でも、ユニオンを介した型パンニングが可能になるということです。
union {
int64_t num;
struct {
int32_t hi, lo;
} parts;
} u = {42};
u.parts.hi = 420;
これはtype-punのgcc保証が機能することです。他のケースは動作するように見えるかもしれませんが、いつの日か静かに壊れる可能性があります。
用語は素晴らしいものです。好きなように使用できます。他の人も使用できます。
バイトで同じサイズの2つのタイプは似ていますか?そうでない場合、同様のタイプは何ですか?
大まかに言えば、型は、それらがconstnessまたはsignnessによって異なる場合、類似しています。バイト単位のサイズだけでは、明らかに十分ではありません。
エイリアスは、タイプが似ているタイプパンニングの特定のケースですか?
型パンニングは、型システムを回避する任意の手法です。
エイリアスは、異なるタイプのオブジェクトを同じアドレスに配置する特定のケースです。タイプが類似している場合、エイリアスは通常許可され、それ以外の場合は禁止されます。さらに、char
(またはchar
)の左辺値を介して任意のタイプのオブジェクトにアクセスできますが、その逆(つまり、タイプchar
のオブジェクトにアクセスする)異なるタイプのlvalue)は許可されません。これはCおよびC++標準の両方で保証されており、GCCは標準が要求するものを実装するだけです。
GCCのドキュメントでは、最後に書き込まれたもの以外のユニオンメンバーを読み取るという狭い意味で「タイプパニング」を使用しているようです。このタイプのパンニングは、タイプが類似していない場合でも、C標準で許可されています。 OTOH C++標準ではこれは許可されていません。 GCCは許可をC++に拡張する場合としない場合があります。ドキュメントはこれについて明確ではありません。
-fstrict-aliasing
がない場合、GCCは明らかにこれらの要件を緩和しますが、正確な範囲は明確ではありません。最適化されたビルドを実行する場合、-fstrict-aliasing
がデフォルトであることに注意してください。
結論として、標準にプログラムするだけです。 GCCが標準の要件を緩和する場合、それは重要ではなく、トラブルに値するものではありません。
C11ドラフトN1570の脚注88によると、「厳密なエイリアシング規則」(6.5p7)は、物事がエイリアシングする可能性をコンパイラが許容しなければならない状況を指定することを意図していますが、どのエイリアシングを定義しようとはしません- is。どこかで、ルールによって定義されたアクセス以外のアクセスが「エイリアス」を表すという一般的な信念が浮上し、許可されたアクセスはそうではありませんが、実際にはその逆が当てはまります。
次のような関数があるとします。
int foo(int *p, int *q)
{ *p = 1; *q = 2; return *p; }
セクション6.5p7は、p
とq
が同じストレージを識別してもエイリアスしないとは述べていません。むしろ、それらがエイリアスする許可であることを指定します。
あるタイプのストレージへのアクセスを含むすべての操作がエイリアスを表すとは限らないことに注意してください。別のオブジェクトから目に見えて派生した左辺値の操作は、その別のオブジェクトを「エイリアス」しません。代わりに、それはisそのオブジェクトに対する操作です。エイリアスは、あるストレージへの参照が作成されてから使用されるまでの間に、同じストレージが何らかの方法で参照された場合に発生します最初から派生していない、またはコードがそのコンテキストに入る場合。
左辺値が別の値から派生したことを認識する機能は実装の品質の問題ですが、標準の作成者は、実装が必須の構造を超えるいくつかの構造を認識することを期待しています。メンバー型の左辺値を使用して、構造体または共用体に関連付けられているストレージにアクセスする一般的な権限はありません。また、規格では明示的に何もしませんsaysomeStruct.member
を含む操作ではsomeStruct
の操作として認識されます。代わりに、標準の作成者は、顧客のニーズをサポートするために妥当な努力を払うコンパイラー作成者は、それらの顧客のニーズを判断して満たすために委員会よりも良い場所に配置されるべきであると予想しました。派生参照を認識するためにかなり合理的な努力をするコンパイラはsomeStruct.member
がsomeStruct
から派生していることに気付くため、標準の作成者は明示的にそれを強制する必要はないと考えました。
残念ながら、次のような構造の扱い:
actOnStruct(&someUnion.someStruct);
int q=*(someUnion.intArray+i)
「actOnStruct
とポインター逆参照がsomeUnion
(およびその結果としてそのすべてのメンバー)に作用することが期待されるべきであることは十分に明らかであり、そのような動作を強制する必要がないこと」から「Since標準では、上記のアクションがsomeUnion
に影響を与える可能性があることを実装が認識することを要求していません。そのような動作に依存するコードは破損しており、サポートする必要はありません。」上記の構成のいずれも、-fno-strict-aliasing
モードを除いて、gccまたはclangで確実にサポートされていません。ただし、それらをサポートすることによってブロックされる「最適化」のほとんどは、「効率的」ですが役に立たないコードを生成します。
そのようなオプションを持つコンパイラで-fno-strict-aliasing
を使用している場合、ほとんど何でも機能します。 iccで-fstrict-aliasing
を使用している場合、エイリアスなしで型パンニングを使用する構成をサポートしようとしますが、それが処理する、または処理しない構成に関する正確なドキュメントがあるかどうかはわかりません。 gccまたはclangで-fstrict-aliasing
を使用する場合、機能するものはすべて偶然によるものです。
ANSI C(AKA C89)では、次のようになります(セクション3.3.2.3構造体と共用体のメンバー):
オブジェクトの別のメンバーに値が格納された後でユニオンオブジェクトのメンバーにアクセスした場合、動作は実装定義です。
C99には(6.5.2.3構造体と共用体のメンバー)があります。
共用体オブジェクトのコンテンツにアクセスするために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されたメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しい型のオブジェクト表現として再解釈されます。 6.2.6で説明されている(「タイプパニング」と呼ばれることもあるプロセス)。これはトラップの表現かもしれません。
IOW、ユニオンベースの型パンニングはCで許可されていますが、実際のセマンティクスはサポートされる言語標準によって異なる場合があります(C99セマンティクスはC89のimplementation-definedより狭いことに注意してください)。
C99には次のようなものもあります(セクション6.5式):
オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つ必要があります。
—オブジェクトの有効なタイプと互換性のあるタイプ
—オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
—オブジェクトの有効なタイプに対応する符号付きまたは符号なしタイプのタイプ
—オブジェクトの有効な型の修飾バージョンに対応する署名付きまたは署名なしの型である型
—メンバーの中に前述のタイプの1つを含む集約またはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれるユニオンのメンバーを含む)、または
—文字タイプ。
また、C99には互換型について説明するセクション(6.2.7互換型と複合型)があります。
2つのタイプのタイプが同じであれば、タイプに互換性があります。 2つの型に互換性があるかどうかを判断するための追加の規則については、型指定子については6.7.2、型修飾子については6.7.3、宣言子については6.7.5で説明しています。 ...
そして(6.7.5.1ポインタ宣言子):
2つのポインター型が互換性を持つためには、どちらも同じように修飾され、どちらも互換型へのポインターでなければなりません。
少し単純化すると、これはCでポインターを使用することにより、signed intにunsigned intとしてアクセスでき(その逆も可能)、個々の文字に何でもアクセスできることを意味します。それ以外のものはエイリアシング違反になります。
C++標準のさまざまなバージョンで同様の言語を見つけることができます。ただし、C++ 03およびC++ 11で確認できる限り、ユニオンベースの型パンニングは明示的に許可されていません(Cとは異なります)。
私が質問をしたとき、UNIONを使用せずにニーズを満たす方法がわからなかったというだけの理由で、補足的な回答を追加するのは良いことだと思います:それは自分のニーズに正確に答えるようだったので、それを使用することに頑固になりました。
型のパンニングを行い、未定義の動作(コンパイラや他の環境設定に依存)の起こり得る結果を回避するための良い方法は、std :: memcpyを使用して、メモリバイトをある型から別の型にコピーすることです。これについては、たとえば here および here のように説明されています。
また、コンパイラーが共用体を使用して型パンニング用の有効なコードを生成すると、std :: memcpyが使用された場合と同じバイナリコードが生成されることもよく読みました。
最後に、この情報が私の元の質問に直接回答しない場合でも、厳密に関連しているため、ここに追加すると便利だと感じました。