Type punning と strict Aliasing Rule
Type punning
とはメモリの中身はそのままにして「別の型として解釈し直す」ことだ。以下のサンプルコードはその一例である。皆さんはこれを見て何を思うか。
int main() {
float x = 2.3;
unsigned int y = *(unsigned int*)(&x); // Type punning
std::printf("x = %d (0x%x)\n", x, y);
}
実は、上記のコードはStrict Aliasing Rule
[1]を違反していてUndefined Behavior
を引き起こしている。
unsigned int y = *(unsigned int*)(&x); // type punning, **Undefined Behavior**
Strict Aliasing Rule
というのは「異なる型(特に非互換のポインタ型)を通して同じメモリにアクセスしてはいけない」というルールで、多くの最適化手法がこのルールを前提にしているため、破るとUBになってしまうのだ。
安全に型を変えるには?
最適化によって振る舞いを変更されるリスクなく安全に上記の操作を実現する方法はただ一つしかない。それは、以下のように別のメモリ領域を確保したうえでコピーする方法だ。
#include <bit>
#include <cstring>
int main() {
float x = 2.3;
// int y = *(int*)(&x); // UB!
int y = std::bit_cast<int>(x); // Ok
int y_;
std::memcpy(&y_, &x, sizeof(int)); // Ok
}
コピーが増えて嫌だなと思うかもしれないが、最適化によって除去される可能性がありますし、何よりUBで不安定になるよりはいい。
ただし注意点として、std::vector
などのコピー方法が固定ではない型(is_trivially_copyable<T> == false
)[2]の場合は、std::memcopy
が使えず、安全に別の型として解釈し直すことはできない。
Aliasingがある型
ただし、型の間にaliasing
がある場合は、ポインタ経由で再解釈しても安全[1:1]。
まず、バイト列(char*
,std::byte
,...)として再解釈することは常に許されている。
float x = 2.3;
char* y = reinterpret_cast<char*>(&x); // Ok
std::cout << (int)y[0] << "\n"; // Ok
また、signed
やunsigned
を付けなおすことも許されている。
int x = -1;
unsigned int* y = reinterpret_cast<unsigned int*>(&x); // Ok
std::cout << *y << "\n";
まとめ
C++のコンパイラはStruct Aliasing Rule
を守っている前提で最適化を頑張ってくれる。その一方で、Strict Aliasing Rule
を破ってもコンパイラは怒ってくれず、気が付かないうちに未定義動作の沼にはまってしまう。けちけちせずstd::memcpy
やstd::bit_cast
を使って安全なコードを書くことを心掛けたい。
今回、このルールに関しては先輩に指摘されて知ることになった。先輩有難う。
Discussion