カーニハンCを読む ついでにJISXも読む 優先度と評価順序 多分わかったんじゃないか劇場
先日来より、C言語の演算子の優先順位でバタバタしていたんだが、kznさんから「JISを読んでみたら?」とコメントいただいて、JISで「優先」を検索してうんうん唸っていたら、ふと視界が開けた気がする。
だからネットって面白い。
kznさんから教えていただいたJISはこちら。
私のバタバタ記事はこちら。
C言語の「++(後置)」は優先順位が高いのである
C言語のインクリメント演算子である「++(後置)」は実は優先順位が高い。(後置)というのは
「++」の演算を後から実行する
という意味で「後置」という。
だから
最も優先順位が高い
などと言われると面食らう。
後から実行するのに何故優先順位が高いのか
優先順位が高いのであれば、「後から」ではなく「最初に」に計算するのではないか
そう思うのが自然である。
だが。
優先順位は高いが後から実行する
これは正しい(多分)。
コンパイラになって考えてみる
もう、ここはコンパイラになった気持ちで考えてみるしかない。
なので、次のコードをコンパイルしてみる。
a[i] = i++
「a」と「i」はメモリに配置されているものとする。
さらに「i = 3」とする。
a[i] = i++ はどう計算するのだろうか。
(1) メモリから i をレジスタ X1 に取り出す。
X1 = 3 となる。
(2) さて、これで i の参照は終わった。
「 = i」が終了したわけである。
従って、ここで i をインクリメントする。
i = 4 となる。
(3) 「a[i]」のアドレスを計算する。
「a」の先頭アドレスに「i」だけ足せばよい。
ところが「i」は既に「4」になっている。
従って、a[4] を計算することになる。
そしてそこに X1 の 3 を代入する。
結果、次のようになる。
a[4] = 3
(2) のインクリメント処理は後置であるため (1) の後に実行される。
だが、他の演算子(この場合は代入演算子「=」)よりも優先順位は高いため (3) の前に実行される。
(1) のステップはC言語のコード上に明確に見えない。だから、「(1)の後」と言われてもピンとこないところはある。また、コンパイルによっては (1) のステップがないかもしれない。
先にリンクした ISO には次のような記載がある。
太字の「結果を取り出した後」が (1) にあたる。
そうだったのか…(  ̄- ̄)。
「結果を取り出した後」でさえあればその後のどこで実行しようが構わないわけだ。「式全体の評価の後」ではない、と。
ちなみに、カーニハンCでは
『使用する前』と『使った後』。
『使った後』と言われると「a[i]に代入した後」と思っちゃうのよね。
でも「結果を取り出した後」と言われるとちょっと違う。そこに「a[i]への代入」が含まれている感じがしない。
実際にコンパイルしてみる
とにかく試してみた。
コンパイラはこれ。
ソースコードはこれ。
#include <stdio.h>
void test1()
{
int i = 3;
char a[10] = {0};
a[i] = i++;
printf("a[3] = %d\n", a[3]);
printf("a[4] = %d\n", a[4]);
printf("i = %d\n", i);
}
int main()
{
test1();
return 0;
}
コンパイルする。
cc -g -gdbx pri.c -o /data/data/com.termux/files/usr/bin/_local/pri
pri.c:7:10: warning: unsequenced modification and access to 'i' [-Wunsequenced]
a[i] = i++;
~ ^
1 warning generated.
なんと!
警告された。
処理順序が定義されていないことに対する警告だ。
ひぇ~。
昨今のコンパイラはこんなことまで警告してくれるのか。ヘタな静的解析よりよくやってくれるんじゃね? clang には静的解析の機能もあった気がするが。
警告はともかく、error ではなく warning であるので、実行モジュールは作成されているはずである。
なので実行してみる。
~ $ pri
a[3] = 0
a[4] = 3
i = 4
~ $
面白いくらいに想定通りの動作であった。
ここまで来たらアセンブラを覗こうではないか
って、そう思いません?
なので、コンパイラにアセンブラを出力してもらった。
.globl test1
// -- Begin function test1
.p2align 2
.type test1,@function
test1: // @test1
.cfi_startproc
// %bb.0:
// (1) スタックを確保
// sp[0]-[9] :a[0]-[10]
// sp[12]-[15]:i
// sp[16]-[23]:x29
// sp[24]-[31]:x30
//
// x29=&sp[16]
// i は [x29, #-4]
// a[0]-[9] は [sp, #0]-[sp, #9]
sub sp, sp, #32 // sp<-sp-32
.cfi_def_cfa_offset 32
// (2) x29 と x30 レジスタをスタックに待避しておく。
stp x29, x30, [sp, #16] // push x29, x30
// 16-byte Folded Spill
add x29, sp, #16 // x29<-sp+16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
// (3) i = 3
mov w8, #3 // w8<-3
stur w8, [x29, #-4] // w8->i
mov x9, sp
// (4) a[10] = {0}
str xzr, [sp] // 0->a[0]-[7]
strh wzr, [sp, #8] // 0->a[8]-[9]
// (5) w8 に i を取り出す
ldur w8, [x29, #-4] // w8<-i
// (6) i++
add w10, w8, #1 // w10<-w8+1
stur w10, [x29, #-4] // w10->i
// (7) a[4] = 3 (a[i++] = i)
ldursw x10, [x29, #-4] // x10<-i
strb w8, [x9, x10] // w8->a[i](3->a[4])
// (8) printf("a[3] = %d\n", a[3]);
ldrb w1, [sp, #3]
adrp x0, .L.str
add x0, x0, :lo12:.L.str
bl printf
// (9) printf("a[4] = %d\n", a[4]);
ldrb w1, [sp, #4]
adrp x0, .L.str.1
add x0, x0, :lo12:.L.str.1
bl printf
// (10) printf("i = %d\n", i);
ldur w1, [x29, #-4]
adrp x0, .L.str.2
add x0, x0, :lo12:.L.str.2
bl printf
.cfi_def_cfa wsp, 32
ldp x29, x30, [sp, #16] // pop x29, x30
// 16-byte Folded Reload
add sp, sp, #32
.cfi_def_cfa_offset 0
.cfi_restore w30
.cfi_restore w29
ret
.Lfunc_end0:
.size test1, .Lfunc_end0-test1
.cfi_endproc // -- End function
まさにココ。
// (5) w8 に i を取り出す
ldur w8, [x29, #-4] // w8<-i
// (6) i++
add w10, w8, #1 // w10<-w8+1
stur w10, [x29, #-4] // w10->i
// (7) a[4] = 3 (a[i++] = i)
ldursw x10, [x29, #-4] // x10<-i
strb w8, [x9, x10] // w8->a[i](3->a[4])
i を取り出してから i++ している(5)(6)。
(5)(6)が終わった時点で
w8 が i
[x29, #-4] が i++
なのねー。
上手いことしてるなぁ。こうやってみると、コンパイラの開発って難しそうだとつくづく感じる。
そして、 a[i] = i++ はというと…
添字の [i] は ++ された i ([x29, #-4])を参照し、
「= i++」は ++ される前の w8 を参照している。
後置なので「= i」が終わってから ++ する。
でも、優先順位が高いので「a[i] = 」よりも先に ++ する。
これで、後置であり、かつ優先順位が高いという、一見相反するような2つの性質が同時に成り立つわけである。
ふぅ~。
そういうことだったのか~(多分)。
スッキリしたー(個人的には)。
この記事が気に入ったらサポートをしてみませんか?