📝

【Java】 s+=VAL + ++s問題

2024/11/24に公開

はじめに

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