Cを高級アセンブラ扱いしているとstrict aliasingに関する未定義動作を引き起こすから注意するようにというツイートを見た。以下駄文。
以下のコードは字面から想定される挙動を示さない。
// clang++ prog.cc -Wall -Wextra -std=gnu++2a #include <iostream> int i{0}; void f(double *ptr) { i = 1; std::cout << i << ":" << &i << " " << *ptr << ":" << ptr<< std::endl; *ptr = 3; std::cout << i << ":" << &i << " " << *ptr << ":" << ptr<< std::endl; } int main() { std::cout << i << ":" << &i << std::endl; f(reinterpret_cast<double*>(&i)); std::cout << i << ":" << &i << std::endl; }
0:0x603194 1:0x603194 4.94066e-324:0x603194 0:0x603194 3:0x603194 0:0x603194
「iの値は3になることを期待しているのにどうしてこうなるか」という問題は、strict aliasing ruleを理解すると納得できる。yohhoyの日記が異常に分かりやすいので読むだけである。結局特定の型の組み合わせでしか、異なる型の値にアクセスすることは規格上許容されておらず、double*
にint*
が渡されることは想定されない。そのためコンパイラはstd::cout << 1
に最適化してしまう(可能性があるし、実際そうなった)ということらしい。未定義動作である。Cスタイルのキャストを使えばコンパイラ時にエラーを見なくてすむが、結果は変わらないのでもっと危険である。reinterpret_castでも未定義動作には変わりない。-fno-strict-aliasingを指定してコンパイルすれば、strict aliasingによる最適化を抑制して直感に反しないアセンブラを吐くことが出来るが、その場しのぎで行儀の悪いコードを放置する付け焼き刃である。unionですらcでは規格内だがc++で規格外(未使用の変数にアクセスできない)なのでmemcpyを使うべきという結論らしい。
shafik/WhatIsStrictAliasingAndWhyDoWeCare.mdも色々記載があって面白そうだが、難しいためまた必要になった際に読むことにする。