🌵

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

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

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

まとめ

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

今回、このルールに関しては先輩に指摘されて知ることになった。先輩有難う。

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

  2. cpp reference jp : is_trivially_copyable ↩︎

Discussion