🌵

Type punning と strict Aliasing Rule

に公開
2

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

また、signedunsignedを付けなおすことも許されている。

int x = -1;
unsigned int* y = reinterpret_cast<unsigned int*>(&x); // Ok
std::cout << *y << "\n";

まとめ

C++のコンパイラはStrict Aliasing Ruleを守っている前提で最適化を頑張ってくれる。気が付かないうちにルールを破り、未定義動作の沼にはまってしまわないよう、けちけちせずstd::memcpystd::bit_castを使って安全なコードを書くことを心掛けたい。

今回、このルールに関しては先輩に指摘されて知ることになった。先輩有難う。より詳しいことはこの資料が参考になる。

追伸、GCCの-Wstrict-aliasingオプション[3]によるコンパイル時検査やclangの-fsanitize=typeオプション[4]による実行時検査によってStrict Aliasing Rule違反を検知できる可能性がある。(詳しくはコメント欄参照)-Wallはつけておいて損がないなぁ。

脚注
  1. cpp reference : type aliasing ↩︎ ↩︎

  2. cpp reference jp : is_trivially_copyable ↩︎

  3. gcc.gnu.org : warning option ↩︎

  4. clang.llvm.org : TypeSanitizer ↩︎

Discussion

yohhoyyohhoy

利用可能(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

h1de_n_nh1de_n_n

情報提供ありがとうございます。

コンパイル時検査や実行時検査のサポートがあるんですね。絶対つけろと言われる-Wallにそんなオプションが埋まっていたとは知りませんでした。。

コメントを受けて、本文を改良しました。ご指摘ありがとうございました。