副作用のタイミング

ありがちダメなコードのダメな理由を書くコーナー。

現象。

C99ソースコード(tmp.c)

#include <stdlib.h>
#include <stdio.h>

int main() {
  int i = 0;

  printf("(++i) + (++i) = %d\n", (++i) + (++i));

  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -g tmp.c
$ ./a.out 
(++i) + (++i) = 4
$ gcc -std=c99 -pedantic -O6 -g tmp.c
$ ./a.out 
(++i) + (++i) = 4

が得られた*1javaだと3になりそうな式なのだが。

説明。

CFAQの3.2(http://www.kouno.jp/home/c_faq/c3.html)とよく似ている。向こうは後置++演算子同士の計算でこちらは前置++演算子同士の計算で、この式は副作用完了点を通過する前にiへの副作用が二回あるから未定義だ、という結論まで同じだ。
なので言語マニア的に設計面から理由付けをする。式の最中に複数の副作用が存在する場合の振る舞いとして選択肢をあげてみよう。

式の途中での副作用自体を禁止する
例えば、pythonでは代入が式ではなく文だ。代入文の中に代入文を入れ子にすることはできないので、式の途中で副作用が起こることはない。式の途中で呼び出した関数の中で副作用を起こすことはできるが、それは別の問題なので次のネタにでもしよう。
式の途中でも副作用を有効にする
例えば、Javaでは副作用のタイミングも引数の評価順序も厳密に定義されており、副作用が幾つあったとしても一切競合しない。JVMなどのスタックマシンを前提にする場合、副作用も逐次的に処理するように定義するのが自然だろう。
式の途中の副作用の競合を未定義とする
上の式のようにC言語が良い例だろう。パイプラインが深いCPUならば左右の式を並行して計算したり、左右の式があまりに複雑なものならば別々のコアが並行して計算したり、といった方式で速度効率を稼ぐ余地を残すことができる。C言語の場合は策定前に公開されていた実装の方式が統一されていなかったというのも理由かもしれない。
式の途中の副作用の競合をエラーとする
C言語仕様での「未定義」はどのような結果になるか判らないという意味、つまり、処理の結果が「コンパイルエラー」であっても良いのではないだろうか。ここまで厳しいコンパイラは聞いたことがないのは、副作用の競合を完全に検出するのは難しい、少なくとも時間がかかる、というのが主な理由だろう。

*1:gccの警告オプション-Wall -Wextraを付けていないのは答えが表示されてしまうのを防ぐため。