yohhoyの日記

技術的メモをしていきたい日記

i += 1 + ++i; の処理結果(Go/Rust編)

異なるプログラミング言語における式i += 1 + ++i処理結果の違いについて。番外編。

// Go
var i = 0
i += 1 + ++i  // ??
// Rust
let mut i = 0;
i += 1 + ++i;  // ??

まとめ:

Go

プログラミング言語Goでは設計判断として前置インクリメント++iを提供しない。また後置インクリメントi++は提供されるが、式(expression)ではなく文(statement)として提供される(→id:yohhoy:20200131)。Go言語FAQより引用。

Why are ++ and -- statements and not expressions? And why postfix, not prefix?
Without pointer arithmetic, the convenience value of pre- and postfix increment operators drops. By removing them from the expression hierarchy altogether, expression syntax is simplified and the messy issues around order of evaluation of ++ and -- (consider f(i++) and p[i] = q[++i]) are eliminated as well. The simplification is significant. As for postfix vs. prefix, either would work fine but the postfix version is more traditional; insistence on prefix arose with the STL, a library for a language whose name contains, ironically, a postfix increment.

https://go.dev/doc/faq#inc_dec

Go言語では代入(=)や代入演算(+=など)は文(statement)として規定され*1、難解な式記述を許さない言語仕様となっている。Simplicity.*2

{ // Java/JavaScript/C#的な動作
  var i = 0;
  i = i + 1 + (i + 1)
  //          ^^^^^^^ ++iの代用
  // 変数 i は 値2
}
{ // C/C++的な動作
  var i = 0
  i++  // ++iの代用
  i += 1 + i
  //       ^ 項++i相当
  // 変数 i は 値3
}

Rust

プログラミング言語Rustでは設計判断としてインクリメント演算子(++)を提供しない*3。Rust言語Frequently Asked Questionsより引用。

Why doesn't Rust have increment and decrement operators?
Preincrement and postincrement (and the decrement equivalents), while convenient, are also fairly complex. They require knowledge of evaluation order, and often lead to subtle bugs and undefined behavior in C and C++. x = x + 1 or x += 1 is only slightly longer, but unambiguous.

GitHub - dtolnay/rust-faq: Frequently Asked Questions · The Rust Programming Language

Rust言語において前置インクリメント++iと等価な式は{ i += 1; i }と記述できる*4。複合代入演算子(+=など)のオペランド評価順はオペランド型に依存して評価順序が変化することに注意*5。両オペランドともプリミティブ型の場合は「右辺→左辺」順で評価が行われる。Here Be Dragons/Hic Sunt Dracones🐉

{ // Java/JavaScript/C#的な動作
  let mut i = 0;
  i = i + 1 + { i += 1; i };
  assert!(i == 2);
  // 部分式の評価順:
  //  i = i + 1 + { i += 1; i };
  // (6) (1) (2)   (4)  (3)(5)
}
{ // C/C++的な動作
  let mut i = 0;
  i += 1 + { i += 1; i };
  assert!(i == 3);
  // 部分式の評価順:
  //  i += 1 + { i += 1; i };
  // (5)  (1)   (3)  (2)(4)
}

Rust言語リファレンスより一部引用(下線部は強調)。

Evaluation order of operands
The following list of expressions all evaluate their operands the same way, as described after the list. Other expressions either don’t take operands or evaluate them conditionally as described on their respective pages.

  • (snip)
  • Arithmetic and logical binary operators
  • (snip)

The operands of these expressions are evaluated prior to applying the effects of the expression. Expressions taking multiple operands are evaluated left to right as written in the source code.

Note: Which subexpressions are the operands of an expression is determined by expression precedence as per the previous section.

For example, the two next method calls will always be called in the same order:

let mut one_two = vec![1, 2].into_iter();
assert_eq!(
    (1, 2),
    (one_two.next().unwrap(), one_two.next().unwrap())
);

Note: Since this is applied recursively, these expressions are also evaluated from innermost to outermost, ignoring siblings until there are no inner subexpressions.

https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands

Compound assignment expressions
(snip)
Evaluation of compound assignment expressions depends on the types of the operators.

If both types are primitives, then the modifying operand will be evaluated first followed by the assigned operand. It will then set the value of the assigned operand's place to the value of performing the operation of the operator with the values of the assigned operand and modifying operand.

Note: This is different than other expressions in that the right operand is evaluated before the left one.

Otherwise, this expression is syntactic sugar for calling the function of the overloading compound assignment trait of the operator (see the table earlier in this chapter). A mutable borrow of the assigned operand is automatically taken.

(snip)

Like assignment expressions, compound assignment expressions always produce the unit value.

⚠️Warning: The evaluation order of operands swaps depending on the types of the operands: with primitive types the right-hand side will get evaluated first, while with non-primitive types the left-hand side will get evaluated first. Try not to write code that depends on the evaluation order of operands in compound assignment expressions. See this test for an example of using this dependency.

https://doc.rust-lang.org/reference/expressions/operator-expr.html#compound-assignment-expressions

関連URL

*1:https://go.dev/ref/spec#Assignment

*2:https://go.dev/talks/2015/simplicity-is-complicated.slide#4

*3:当然ながら、Rust言語ではデクリメント演算子 -- も提供されない。

*4:Rust言語では加算代入 i += 1 の評価結果はユニット値(unit value)となり、0 + (i += 1) のような式は型不整合によるコンパイルエラーとなる。

*5:代入演算子オーバーロード機能を提供するC++にも同様のオペランド評価順問題があり、C++17以降では「ユーザ型向けにオーバーロードされていても、プリミティブ型に対するオペランド評価順と統一する」規則を追加導入している。