【Java】 s+=VAL + ++s問題
はじめに
Javaでs+=VAL + ++s
というコードを書いた場合、16になる理由が分からないという話を聞いたので調べてみた。先に、結論を書くとこれは理解すべき問題ではない。最初にそういう実装してしまったので互換性を保つためにそうしていることを知識として覚えておけと言う話になる。
この計算をclangでコンパイルすると以下のようなワーニングが出る。
test.c:6:14: warning: unsequenced modification and access to 's' [-Wunsequenced]
6 | s += VAL + ++s;
| ~ ~
これは一つの式で二回以上の変更を同じ変数に行うことはC言語では定義されていないことを示している。つまり使ってはいけないのである。
JavaとC言語の違い
次にJavaで書いた場合とC言語で書いた場合の違いを見てみる。
public class test {
public static void main(String[] args) {
int VAL=3;
int s = 6;
s = s + VAL + ++s;
System.out.println(s);
}
}
16
Javaでは16が出力される。
#include <stdio.h>
void main(void) {
int VAL = 3;
int s = 6;
s += VAL + ++s;
printf("s = %d\n", s);
}
17
C言語では17が出力される。つまり、JavaとC言語では違う結果になるのである。これはCで書いたプログラムをJavaに移植する際に気をつけるべき点であるために覚えておくべき事項になるわけである。そのためJavaの資格試験に出題されるのだろう。
分析
上記Cコードをgccでアセンブラを出力した結果が以下のようになる。
.file "test.c"
.text
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $3, -8(%rbp) ; VAL =3
movl $6, -4(%rbp) ; s = 6
addl $1, -4(%rbp) ; ++s (s += 1)
movl -4(%rbp), %edx ; edx = s
movl -8(%rbp), %eax ; eax = VAL
addl %edx, %eax ; tmp = VAL +s
addl %eax, -4(%rbp) ; s += tmp
movl -4(%rbp), %eax ; 以下printfのための処理
movl %eax, %esi
leaq .LC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 13.2.0-23ubuntu4) 13.2.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
これは、s += VAL + ++s;
が以下のように計算されていることを示している
s += 1;
s += VAL + s;
ところがJavaでは以下のようにコンパイルされている。
public class test {
public test() {
}
public static void main(String[] args) {
int VAL = 3;
int s = 6;
int var10000 = s + VAL;
++s;
s += var10000;
System.out.println(s);
}
}
他のパターンを調べると ++sが、s++に置換されていた。したがって実際には以下のよう展開されるらしい。
temp = VAL + s;
s++;
s += temp;
どうやら++s は s++ に置換され、後から実行される様に展開されるらしい。どうしてこの挙動になるかの理解は難しい。Javaの最初のコンパイラがその様に実装されたために、互換性を保つためにそのようになっていると考えられる。これは実装上の問題であるため理解するのは難しい。理解するものではない。単純に覚えておくべき事項だろう。
追記
以下のコードを考えてみる。
t = ++s + VAL ++s;
この場合はどうなるか?このケースでは、JavaもCも18になった。一つの式で、同じ変数に二回以上代入が発生する場合、Javaは代入演算子を使うか、使わないかで挙動が異なる様である。最初にも書いた様に、このコード自体がclangで未定義の挙動とされているため、この様なコードは書くべきではないだろう。
Discussion