C++ エイリアスと最適化の危険な関係

エイリアス

エイリアス(alias) とは別名という意味です
何の別名かというと lvalue の別名ということです

他のオブジェクトのインスタンス(実体)を参照しているものがエイリアスです

文法的にはポインタと参照がエイリアスです

lvalue と rvalue

エイリアスが lvalue の別名であるといいましたが、この lvalue とは何でしょう

かつて lvalue と rvalue というのは代入演算子の左辺か右辺というくらいの意味しかありませんでした
しかし今はそれぞれ独立した概念となっていて次期標準ではさらに細分化されます

現在の標準においては大雑把にいえば lvalue というのは名前の付いたオブジェクトのインスタンスのことで、rvalue というのは名前の無い一時的なもののことです

  • lvalueの例
    • 名前の付いている変数や関数は全部 lvalue です
    • その要素やメンバも lvalue です
    • 非const参照は lvalue です
    • ポインタをデリファレンスしたものは lvalue です
  • rvalueの例
    • 数値リテラルや文字リテラルは rvalue です
    • 参照でない関数の戻り値やキャストの戻り値は rvalue です

strict-aliasing-rules

標準では 3.10.15 でエイリアスについての言及があります

3 Basic concepts
 3.10 Lvalues and rvalues
  15 If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined. (52
     - the dynamic type of the object,
     - a cv-qualified version of the dynamic type of the object,
     - a type similar (as defined in 4.4) to the dynamic type of the object,
     - a type that is the signed or unsigned type corresponding to the dynamic type of the object,
     - a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
     - an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, a an element or non-static data member of a subaggregate or contained union),
     - a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
     - a char or unsigned char type.
     
  52) The intent of this list is to specify those circumstances in which an object may or may not be aliased.

プログラムがオブジェクトに格納されている値に以下の型以外の lvalue を介してアクセスした場合の動作は未定義とする
(注: このリストはどのような状況においてオブジェクトにエイリアスがあるのかどうかを明確にするものである)

  • オブジェクトの動的な型そのものでアクセスする場合があるのは当然
  • const や volatile は違っていても構わない
  • signed や unsigned も違っていても構わない
  • signed や unsigned と const や volatile の両方が違っていても構わない
  • 上記の型を要素とする配列を介してアクセスしても構わない

    また上記の型をメンバとする構造体や union を介してアクセスしても構わない
  • オブジェクトの動的な型の基底クラスを介してアクセスしても構わない
  • char あるいは unsigned char を介してアクセスしても構わない

○ signed や unsigned の有無は気にしない

int n = 10;
unsigned int* pu = reinterpret_cast<unsigned int*>(&f); // pu は合法なエイリアス
unsigned int u = *pf;  // 問題なし

○ char 単位でのアクセスは常に合法

float f = 1.0f;
char* pc = reinterpret_cast<char*>(&f); // pc は合法なエイリアス
char c = *pc;  // 問題なし

× signed char と char は関係ない型なのでダメ。かな?

float f = 1.0f;
signed char* pc = reinterpret_cast<signed char*>(&f); // pc は不正なエイリアス
signed char c = *pc;  // 不正エイリアスを介したアクセスは未定義動

× こんなのはダメ

float f = 1.0f;
int* pi = reinterpret_cast<int*>(&f); // pi は不正なエイリアス
int i = *pi;  // 不正エイリアスを介したアクセスは未定義動作

これを strict-aliasing-rules といいます

strict-aliasing-rules を守るには

strict-aliasing-rules を守るには格納されている値の型と無関係な型でアクセスしなければいいということなので、ある関数内の閉じた文脈については下記のようなコードが無ければ安全です

  • reinterpret_cast で無関係な型にキャストしている場合は危険です
  • Cスタイルのキャストで reinterpret_cast 相当の挙動を期待している場合も reinterpret_cast と同様に危険です
  • void* を介するコードはそこで型の関連性のチェックが切れてしまうのでこれも reinterpret_cast と同様に危険です

それ以外では次のようなものがあると危険です

  • memset などのメモリブロック操作系の関数
  • malloc などのヒープ関連
  • ファイルやネットワークなどとの入出力
  • 可変長引数

strict-aliasing-rules というのは格納された値と違う型では一切アクセスしてはいけないというルールなので、ある範囲のデータを C++ の型とは関係なく一塊のバイナリとして扱うようなコードを呼び出しているところは全部ダメです
strict-aliasing-rules が適用されて問題のないコードというのが非常に限られたものであることがわかると思います

strict-aliasing-rules を前提とした最適化

コンパイラによっては strict-aliasing-rules を前提とした最適化をしてくれるオプションがあったりしますが、この strict-aliasing-rules を前提とした最適化は有効である半面非常に危険である場合があるので、この最適化を適用する範囲についてはよくよく検討する必要があります

オプティマイザが strict-aliasing-rules に従っているコードが渡されてくることを期待しているのにそうでないコードを渡してしまった場合、意図しないコードが生成される場合があります

安易に全体に対してこの最適化を適用しないように気をつけてください

でも

このように strict-aliasing-rules の下に於いてはメモリブロックをコピーすることや、オブジェクトのアドレスを元の型と関係ない型のポインタに入れたりすること自体が不法行為のように扱われますが、標準ではもちろんどちらも何の問題もないと明記されています

例えば trivially copiable

3 Basic concepts
 3.9 Types
  2 For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char. (39
    If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value.
  [ Example:
  
  #define N sizeof(T)
  char buf[N];
  T obj; // obj initialized to its original value
  std::memcpy(buf, &obj, N); // between these two calls to std::memcpy, obj might be modified
  std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type holds its original value
  
  end example ]
  
  39) By using, for example, the library functions (17.6.1.2) std::memcpy or std::memmove.

memcpy と memmove の中身がバイト単位の転送じゃないといけないなんて仕様はありません
では strict-aliasing-rules の下で一体どのように実装すればいいというのでしょうか
ヒープの実装も可変長引数の実装もそうです。一体どのように実装すればいいというのでしょう

strict-aliasing-rules に char 単位のアクセスを認めるという項目があることからわかるように、あのリストは基本的にはエンディアンが念頭にあってのリストであって、4バイトの値を書いたときにどっちのワードが先に置かれるのかというのは未定義であるというようなことを言っているだけだと考えられます
考えられます。というのは個人的な見解ですけど
でもそうでないと memcpy も memset も可変長引数もヒープも何も使えないことになってしまいますからね

それでも

それでも現実問題として一部のコンパイラオプティマイザはあのリストを錦の御旗に、不正なコードを出力しますので、strict-aliasing-rules を前提とした最適化には十分に気をつけてください

たぶん

たぶん strict-aliasing-rules は標準の不備なんだと思う
ちょっと言葉が足りないんじゃないかな