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. C++20以降はこちらが推奨
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++のコンパイラはStrict Aliasing Ruleを守っている前提で最適化を頑張ってくれる。気が付かないうちにルールを破り、未定義動作の沼にはまってしまわないよう、けちけちせずstd::memcpyやstd::bit_castを使って安全なコードを書くことを心掛けたい。
今回、このルールに関しては先輩に指摘されて知ることになった。先輩有難う。より詳しいことはこの資料が参考になる。
追伸、GCCの-Wstrict-aliasingオプション[3]によるコンパイル時検査やclangの-fsanitize=typeオプション[4]による実行時検査によってStrict Aliasing Rule違反を検知できる可能性がある。(詳しくはコメント欄参照)-Wallはつけておいて損がないなぁ。
Discussion
利用可能(C++20以降)ならば、コンパイル時に型サイズ互換性検査を行う
std::bit_castが望ましいですね。std::memcpyはC++20未満環境での選択肢と考えるのがベターと思います。Strict Aliasing Rule違反は、GCCであれば
-Wstrict-aliasing(または-Wall) オプションによるコンパイル時検査、Clangであれば-fsanitize=typeオプションによる実行時検査でも検出可能です。(VisualC++はStrict Aliasing Ruleによる積極的な最適化を行わないため本件と無縁)Demo: https://godbolt.org/z/vMPP7qzdf
情報提供ありがとうございます。
コンパイル時検査や実行時検査のサポートがあるんですね。絶対つけろと言われる
-Wallにそんなオプションが埋まっていたとは知りませんでした。。コメントを受けて、本文を改良しました。ご指摘ありがとうございました。