異なるプログラミング言語における式i += 1 + ++i
処理結果の違いについてメモ。*1
警告:1つの式内で同一変数を複数回更新する技巧的なコード記述は避けてください。こんな式を書くもんじゃねぇよ言語仕様書の紙束で張っ倒すぞ(#ノ゚Д゚)ㇸ📘 ...取り乱しました。
int i = 0; // Java,C#,C++,C // let i = 0; // JavaScript i += 1 + ++i; // 変数 i の値は?
まとめ:整数型の変数i
が値0
で初期化さているとき、下記プログラミング言語における処理結果は次の通り。
- Java:値
2
- JavaScript:値
2
- C#:値
2
- C:未定義動作(undefined behavior)(→id:yohhoy:20241125)
- C++(→id:yohhoy:20241125)
複合代入演算子+=
の左辺変数i
値を “どのタイミングで読み取るか” により結果が異なる。Java/JavaScript/C#ではi = i + (1 + ++i)
相当と解釈され、いずれも値2
が言語仕様により保証された結果である。*2
Java
式i += 1 + ++i
は下記順序で評価され、最終的に変数i
は値2
となる。
- 複合代入演算子
+=
左辺(変数i
)の値0
を保存しておく(★L)。 - 複合代入演算子
+=
右辺(1 + ++i
)を評価し、値2
をえる(★R)。 - 複合代入演算子
+=
の計算プロセスとして、手順1:保存した左辺の値(★L)と手順2:右辺の評価結果(★R)に対して加算操作(+
)を適用し、値2
をえる。 - 手順3の評価結果
2
を、複合代入演算子+=
左辺(変数i
)に代入する。
複合代入演算子の評価プロセスについて、Java Language Specification(JLS) Java SE 23 Edition, 15.26.2より一部引用(下線部は強調)。*3
A compound assignment expression of the form
E1 op= E2
is equivalent toE1 = (T) ((E1) op (E2))
, whereT
is the type ofE1
, except thatE1
is evaluated only once.
(snip)
At run time, the expression is evaluated in one of two ways.If the left-hand operand expression is not an array access expression, then:
JLS 23, 15.26.2 Compound Assignment Operators
- First, the left-hand operand is evaluated to produce a variable. If this evaluation completes abruptly, then the assignment expression completes abruptly for the same reason; the right-hand operand is not evaluated and no assignment occurs.
- Otherwise, the value of the left-hand operand is saved and then the right-hand operand is evaluated. If this evaluation completes abruptly, then the assignment expression completes abruptly for the same reason and no assignment occurs.
- Otherwise, the saved value of the left-hand variable and the value of the right-hand operand are used to perform the binary operation indicated by the compound assignment operator. If this operation completes abruptly, then the assignment expression completes abruptly for the same reason and no assignment occurs.
- Otherwise, the result of the binary operation is converted to the type of the left-hand variable, and the result of the conversion is stored into the variable.
JavaScript
式i += 1 + ++i
は下記順序で評価され、最終的に変数i
は値2
となる。
- 複合代入演算子
+=
左辺(変数i
)の値0
を保存しておく(lval)。 - 複合代入演算子
+=
右辺(1 + ++i
)を評価し、値2
をえる(rval)。 - 複合代入演算子
+=
の計算プロセスとして、保存した左辺の値(lval)と右辺の評価結果(rval)に対して加算操作(+
)を適用し、値2
をえる(r)。 - 手順3の評価結果
2
(r)を、複合代入演算子+=
左辺(変数i
; lref)に代入する。
複合代入演算子の評価プロセスについて、ECMAScript 2024 Language Specification 15th edition, 13.15.2より一部引用。*4
AssignmentExpression : LeftHandSideExpression AssignmentOperator AssignmentExpression
ECMAScript 2024 Language Specification, 13.15.2 Runtime Semantics: Evaluation
- Let lref be
? Evaluation
of LeftHandSideExpression.- Let lval be
? GetValue(lref)
.- Let rref be
? Evaluation
of AssignmentExpression.- Let rval be
? GetValue(rref)
.- Let assignmentOpText be the source text matched by AssignmentOperator.
- Let opText be the sequence of Unicode code points associated with assignmentOpText in the following table:
- assignmentOpText
+=
/ opText+
- (snip)
- Let r be
? ApplyStringOrNumericBinaryOperator(lval, opText, rval)
.- Perform
? PutValue(lref, r)
.- Return
r
.
C#
式i += 1 + ++i
は式i = i + (1 + ++i)
として下記順序で評価され、最終的に変数i
は値2
となる。
- 複合代入演算子
+=
左辺(変数i
)を評価し、値0
をえる。 - 加算演算子
+
左辺(1
)を評価し、値1
をえる。 - 加算演算子
+
右辺(++i
)を評価し、変数i
の値を0
→1
に更新したのち、値1
をえる。 - 手順1〜3評価結果から
0 + (1 + 1)
→値2
を算出し、複合代入演算子+=
左辺(変数i
)に代入する。
演算子オペランドの評価順序および複合代入演算子の評価プロセスについて、ECMA-334 6th edition*5, 11.4.1, 11.18.2, 11.18.3より一部引用。
11.4 Operators
11.4.1 General
Expressions are constructed from operands and operators. The operators of an expression indicate which operations to apply to the operands.(snip)
The order of evaluation of operators in an expression is determined by the precedence and associativity of the operators (§11.4.2).
Operands in an expression are evaluated from left to right.
Example: In
F(i) + G(i++) * H(i)
, methodF
is called using the old value ofi
, then methodG
is called with the old value ofi
, and, finally, methodH
is called with the new value ofi
. This is separate from and unrelated to operator precedence. end example
11.18.2 Simple assignment
The run-time processing of a simple assignment of the formx = y
consists of the following steps:
- If
x
is classified as a variable:
x
is evaluated to produce the variable.y
is evaluated and, if required, converted to the type ofx
through an implicit conversion (§10.2).- If the variable given by
x
is an array element of a reference_type, (snip)- The value resulting from the evaluation and conversion of
y
is stored into the location given by the evaluation ofx
.- (snip)
11.18.3 Compound assignment
An operation of the form
x «op»= y
is processed by applying binary operator overload resolution (§11.4.5) as if the operation was writtenx «op» y
. Then,
- If the return type of the selected operator is implicitly convertible to the type of
x
, the operation is evaluated asx = x «op» y
, except thatx
is evaluated only once.- (snip)
The term "evaluated only once" means that in the evaluation of
x «op» y
, the results of any constituent expressions ofx
are temporarily saved and then reused when performing the assignment tox
.
関連URL
*1:謝辞:本記事は https://twitter.com/YojoPaisen/status/1859465566324654494 および https://twitter.com/it_muitenai/status/1859163754644083193 経由で頂いた情報をベースにしています。
*2:JavaおよびJavaScript(ECMAScript)言語仕様の解釈には曖昧さが無い。一方でC#言語の仕様記述は曖昧さが残るため、本文中の仕様解釈に多少の疑念がないわけでもない。これ以外の解釈とする必然性もないし、たぶん合ってるんじゃない?
*3:JLS 同セクションの Example 15.26.2-2. Value Of Left-Hand Side Of Compound Assignment Is Saved Before Evaluation Of Right-Hand Side にて本記事との類似ケースが例示説明されている。
*4:ECMAScript言語仕様のRuntime Semantics記述に頻出する ? 記号は、仮想操作ReturnIfAbrupt適用を表す略記用プレフィクス(prefix)である。本記事においては例外を扱わないため、単に無視してもよい。詳細は§5.2.3.4 ReturnIfAbrupt Shorthands参照のこと。
*5:https://ecma-international.org/wp-content/uploads/ECMA-334_6th_edition_june_2022.pdf