前々回、前回と strict aliasing rule について紹介しましたが今回は一般的なalias(別名)の問題について紹介します。
以下の関数は深刻な問題を抱えています。お分かりいただけるでしょうか。
// x*2 + x*2を計算し結果をyに格納する。 ただしxは十分に小さな値とする。 void calc(unsigned& y, const unsigned& x) { y = x*2; y += x*2; }
コンパイラはソース通りにコードを生成するでしょう。しかしそれはプログラマが期待した仕様(コメントに書いてあるもの)とは異なります。
xとして1を入力すれば4が得られるとプログラマは期待しますが以下のように呼び出した場合は6が得られます。
unsigned i = 1; calc(i, i); // i == 6
つまりxとyは同じオブジェクトを指しておりaliasの関係にあるため以下のように計算されていることになります。
unsigned i = 1; i = i*2; i += i*2;
x にはconstが付いていますがこのconstは x を経由した場合に変更できないだけであってyならば元のiはいくらでも変更可能です。
このcalc関数においてxとyがaliasの関係になりうることをプログラマが考慮していないために問題が起きています。コンパイラは同じ型(互換性のある型)同士aliasになり得ることを考慮したコード生成を行っています。
この例を解決するにはローカルな変数を利用するのがよいです(変数を使わず1行で書いてもよい)。最適化が効きやすくなる副次効果もあります。
void calc(unsigned& y, const unsigned& x) { unsigned tmp = x*2; y = tmp + x*2; }
さらに理想的なインターフェイスを目指すなら出力は戻り値にまとめましょう(複数の値を返す時は専用の構造体などを利用)。引数のunsigned intはサイズも小さいので値渡しにしましょう。値渡しならば原理的にエイリアスの問題とは無縁です。
int calc(unsigned x) { return x*2 + x*2; }
このエイリアスの問題はどんなところでも(また別言語においても)起こり得る問題ですが真っ先に思い付くのは代入演算子における自己代入でしょう。
struct s { char arr[32]; s& operator = (const s& rhs) { //本来ならばわざわざ定義する必要もないですが memcpy(this->arr, rhs.arr, 32); return *this; } }; s v; v = v; //undefined behavior
上記コードのように自己代入した場合に(*thisとrhsは同一のオブジェクトであるから)同じアドレスをmemcpyに渡してしまい未定義動作となります。memmoveならば問題ありません。もしくはthisとrhsのアドレスを比較して同じ場合は処理を飛ばす手もあります。(自己代入ならば普通は何もしなくても結果は同じでしょう)。
他に自己代入でよくありがちなのはメモリリソースを手動管理している状況で、代入先のメモリを開放したら代入元のメモリまで開放されてしまい解放済みメモリへのアクセス&二重開放となる場合などです。
またシーケンスポイントがない場合にも注意しましょう。以下の例は未定義動作を引き起こします。
//組み込み型のint iをシーケンスポイントを挟まず複数回変更している int i=0; i = i++; //未定義動作
この場合まだ分かり易いですが以下のように名前が異なる場合は見落としやすくなります。
int i=0; int& j = i; i = j++; //未定義動作 func(++i, ++j); //未定義動作
常にaliasの存在(同一オブジェクトを指すポインタや参照の存在)を考慮し、aliasが存在しても問題の無いようにする必要があります。