[C/C++][コンパイラ] オペランド評価の実際

こことか

こことか

で話されているオペランドの評価に関してだけれども, もちろん処理系依存なので一般的な議論はできないが,実際のところコンパイラの実装がどうなっているかは興味がある所ではないかと思う。

例えばC Q&Aの例の

int i = 7;
printf("%d", i++ * i++);

が49になるのは, まぁ納得できる人は多いと思うけれども,

int i = 7;
printf("%d", ++i * ++i);

が81になると言ったら(つまり2つの++iがどちらも9になる), 驚く人は結構いるのではないかと思う。
実はこれは何も奇妙な話ではなく,コンパイラを素直に実装するとこうなってしまう。多くのコンパイラで同じになっているのではないだろうか。

説明

プログラムは何でも機械語まで落ちるわけなので, コンパイルの途中で直列化を行う必要がある。例えば

op(e1, e2, ..., en)

というオペランドがn個ある命令を直列化する場合には

e1を直列化したもの
e2を直列化したもの
...
enを直列化したもの
op(e1の評価値, e2の評価値, ..., enの評価値)

という風になる。(並べ方は自由だけれども, 第1オペランドから並べるのがまぁ自然でしょう)

具体例をあげると

x = y++;

t = y;
y = y + 1;
x = t;  /* tがy++の評価値 */

とか

x = y;
y = y + 1;

とかになる。(実装は前者が楽だが,後者の方が効率が良く,例えばgccは主に後者を使う.)
++yの場合は「yそのものを評価値として使用」して

x = ++y;

y = y + 1;
x = y;

としてしまう事が多い。


以上を踏まえて冒頭の例を考えて見ると,

i = 7;
x = ++i * ++i;

i = 7;
i = i + 1; /* 左の++i (iが評価値) */
i = i + 1; /* 右の++i (iが評価値) */
x = i * i;

となるので++i*++iが81になるわけである。


次に

#include <stdio.h>
void f(int x;int y;int z){
        printf(“%d %d %d\n”,x,y,z);
}
int main(){
        int n=10;
        f(n++, --n, ++n);
        return 0;
}

の出力がなぜ

10 11 11

となるかだけれども,

n = 10;
f(n++, --n, ++n);

を直列化すると

n = 10;
t = n;      /* tがn++の評価値 */
n = n + 1;
n = n - 1; /* このnが--nの評価値 */
n = n + 1; /* このnが++nの評価値 */
f(t, n, n);

となるので,f(10, 11, 11)という呼び出しになると説明できる。


以上の話は, もちろん規格で何も定められていない話なのでさまざまな実装がありえるし, 同一の実装でも最適化などの具合によっていくらでも変わりうる話であるという事に注意。
ただ, 未定義だからとそれですませるのではなく,実際の所どうなっているのかに興味を持ってみるのも面白いのでは無いかと思う。