これは、奇妙なキャストやユニオンなどを使用してどのようなゆがみを実行しても、決して合法ではありません。
基本的な事実は次のとおりです。異なる型の 2 つのオブジェクトは、いくつかの特別な例外を除いて、メモリ内で決して別名になることはありません (さらに下を参照)。
例
次のコードを検討してください。
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
out += *in++;
}
}
これをローカル レジスタ変数に分割して、実際の実行をより厳密にモデル化しましょう。
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
register double out_val = out; // (1)
register double in_val = *in; // (2)
register double tmp = out_val + in_val;
out = tmp; // (3)
in++;
}
}
(1)、(2)、(3) がそれぞれメモリの読み取り、読み取り、書き込みを表しているとします。これは、このようなタイトな内部ループでは非常にコストのかかる操作になる可能性があります。このループの合理的な最適化は次のようになります。
void sum(double& out, float* in, int count) {
register double tmp = out; // (1)
for(int i = 0; i < count; ++i) {
register double in_val = *in; // (2)
tmp = tmp + in_val;
in++;
}
out = tmp; // (3)
}
この最適化により、必要なメモリ読み取り数が半分になり、メモリ書き込み数が 1 に減少します。これは、コードのパフォーマンスに大きな影響を与える可能性があり、すべての最適化 C および C++ コンパイラにとって非常に重要な最適化です。
ここで、厳密なエイリアシングがないとします。任意のタイプのオブジェクトへの書き込みが他のオブジェクトに影響を与える可能性があるとします。double への書き込みがどこかの float の値に影響を与える可能性があるとします。これにより、上記の最適化が疑わしいものになります。なぜなら、プログラマーが実際に out と in にエイリアスを意図していた可能性があるため、 sum 関数の結果がより複雑になり、プロセスの影響を受ける可能性があるからです。ばかげているように聞こえますか?それでも、コンパイラは「愚かな」コードと「賢い」コードを区別できません。コンパイラは、整形式のコードと不正な形式のコードのみを区別できます。フリーエイリアシングを許可する場合、コンパイラは最適化において保守的である必要があり、ループの各反復で余分なストア (3) を実行する必要があります。
ユニオンやキャストのトリックが合法ではない理由が理解できたと思います。このような基本的な概念を巧妙に回避することはできません。
厳密なエイリアシングの例外
C および C++ 標準char
では、クラス メンバーのアドレスを独立して使用できることが非常に重要であるため、派生型、基本型、およびメンバーを含む「関連型」を使用して、任意の型をエイリアス化するための特別な規定が設けられています。この回答では、これらの規定の完全なリストを見つけることができます。
さらに、GCC は、最後に書き込まれたものとは異なる共用体のメンバーから読み取るための特別な規定を作成します。この種のユニオンによる変換では、実際にはエイリアシングに違反することはできないことに注意してください。一度にアクティブにできる共用体のメンバーは 1 つだけです。たとえば、GCC を使用しても、次の動作は未定義です。
union {
double d;
float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
// a union as simultaneously active
回避策
あるオブジェクトのビットを別の型のオブジェクトのビットとして再解釈する唯一の標準的な方法は、 と同等のものを使用することですmemcpy
。これにより、オブジェクトのエイリアシングのための特別な規定が利用され、事実上、基になるオブジェクト表現をバイト レベルでchar
読み取ったり変更したりできるようになります。たとえば、以下は正当であり、厳密なエイリアシング ルールに違反していません。
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));
これは、次のコードと意味的に同等です。
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
((char*)a)[i] = ((char*)&d)[i];
GCC は、非アクティブな共用体メンバーからの読み取りを規定し、暗黙的にそれをアクティブにします。GCC のドキュメントから:
最後に書き込まれたものとは異なるユニオン メンバーから読み取る (「タイプ パニング」と呼ばれる) 慣行は一般的です。-fstrict-aliasing を使用しても、union 型を介してメモリにアクセスする場合は、型パニングが許可されます。したがって、上記のコードは期待どおりに機能します。構造体共用体列挙とビットフィールドの実装を参照してください。ただし、このコードでは次のことができない場合があります。
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
同様に、アドレスを取得し、結果のポインターをキャストし、結果を逆参照することによるアクセスには、キャストがユニオン型を使用している場合でも、未定義の動作があります。
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
新しいプレースメント
(注:現在、標準にアクセスできないため、ここでは記憶で行っています)。オブジェクトをストレージ バッファーに新規配置すると、基になるストレージ オブジェクトの有効期間が暗黙的に終了します。これは、共用体のメンバーに書き込む場合と似ています。
union {
int i;
float f;
} u;
// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.
それでは、placement-new で同様のものを見てみましょう。
#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.
この種の暗黙的な有効期間の終了は、明らかな理由から、単純なコンストラクターとデストラクターを持つ型でのみ発生します。