ChatGPT解决这个技术问题 Extra ChatGPT

What made i = i++ + 1; legal in C++17?

Before you start yelling undefined behaviour, this is explicitly listed in N4659 (C++17)

  i = i++ + 1;        // the value of i is incremented

Yet in N3337 (C++11)

  i = i++ + 1;        // the behavior is undefined

What changed?

From what I can gather, from [N4659 basic.exec]

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a memory location is unsequenced relative to either another side effect on the same memory location or a value computation using the value of any object in the same memory location, and they are not potentially concurrent, the behavior is undefined.

Where value is defined at [N4659 basic.type]

For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values

From [N3337 basic.exec]

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.

Likewise, value is defined at [N3337 basic.type]

For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values.

They are identical except mention of concurrency which doesn't matter, and with the usage of memory location instead of scalar object, where

Arithmetic types, enumeration types, pointer types, pointer to member types, std::nullptr_t, and cv-qualified versions of these types are collectively called scalar types.

Which doesn't affect the example.

From [N4659 expr.ass]

The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand.

From [N3337 expr.ass]

The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression.

The only difference being the last sentence being absent in N3337.

The last sentence however, shouldn't have any importance as the left operand i is neither "another side effect" nor "using the value of the same scalar object" as the id-expression is a lvalue.

You have identified the reason why: In C++17, the right operand is sequenced before the left operand. In C++11 there was no such sequencing. What, precisely, is your question?
@Robᵩ See last sentence.
Does anyone have a link to the motivation for this change? I would like a static analyser to be able to say "you don't want to do that" when faced with code like i = i++ + 1;.
@NeilButterworth, it's from the paper p0145r3.pdf: "Refining Expression Evaluation Order for Idiomatic C++".
@NeilButterworth, section number 2 says that this is counter intuitive and even experts fail to do the right thing in all cases. That's pretty much all their motivation.

A
AnT stands with Russia

In C++11 the act of "assignment", i.e. the side-effect of modifying the LHS, is sequenced after the value computation of the right operand. Note that this is a relatively "weak" guarantee: it produces sequencing only with relation to value computation of the RHS. It says nothing about the side-effects that might be present in the RHS, since occurrence of side-effects is not part of value computation. The requirements of C++11 establish no relative sequencing between the act of assignment and any side-effects of the RHS. This is what creates the potential for UB.

The only hope in this case is any additional guarantees made by specific operators used in RHS. If the RHS used a prefix ++, sequencing properties specific to the prefix form of ++ would have saved the day in this example. But postfix ++ is a different story: it does not make such guarantees. In C++11 the side-effects of = and postfix ++ end up unsequenced with relation to each other in this example. And that is UB.

In C++17 an extra sentence is added to the specification of assignment operator:

The right operand is sequenced before the left operand.

In combination with the above it makes for a very strong guarantee. It sequences everything that happens in the RHS (including any side-effects) before everything that happens in the LHS. Since the actual assignment is sequenced after LHS (and RHS), that extra sequencing completely isolates the act of assignment from any side-effects present in RHS. This stronger sequencing is what eliminates the above UB.

(Updated to take into account @John Bollinger's comments.)


Is it really correct to include "the actual act of assignment" in the effects covered by "the left hand operand" in that excerpt? The standard has separate language about the sequencing of the actual assignment. I take the excerpt you've presented to be limited in scope to the sequencing of the left-hand and right-hand sub-expressions, which does not seem to be sufficient, in combination with the rest of that section, to support well-definedness of the OP's statement.
Correction: the actual assignment is still sequenced after the value computation of the left operand, and evaluation of the left operand is sequenced after (complete) evaluation of the right operand, so yes, that change is sufficient to support the well-definedness the OP asked about. I'm just quibbling the details, then, but these do matter, as they might have different implications for different code.
I don't disagree, @supercat, but the standard says what it says, and conforming programs must comply. That's in an "as if" sense, however: a program has a great deal of internal leeway, provided that its external behavior is exactly as if it operated strictly as the standard specifies. With regard to your particular example, the catch is of course whether the compiler can prove that foo() will not modify globalVariable. Unless it can do so, it needs to make that temporary copy before performing the function call.
@JohnBollinger: I find it curious that the authors of the Standard would make a change that impairs the efficiency of even straightforward code generation and has historically not been necessary, and yet balk at defining other behaviors whose absence is a much bigger problem, and which would seldom pose any meaningful impediment to efficiency.
@Kaz: For compound assignments, performing the left-side value evaluation after the right side allows something like x -= y; to be processed as mov eax,[y] / sub [x],eax rather than mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. I don't see anything idiodic about that. If one had to specify an ordering, the most efficient ordering would probably be to perform all computations necessary to identify the left-side object first, then evaluate the right operand, then the value of the left object, but that would require having a term for the act of resolving the left object's id'ty.
佚名

You identified the new sentence

The right operand is sequenced before the left operand.

and you correctly identified that the evaluation of the left operand as an lvalue is irrelevant. However, sequenced before is specified to be a transitive relation. The complete right operand (including the post-increment) is therefore also sequenced before the assignment. In C++11, only the value computation of the right operand was sequenced before the assignment.


L
Lundin

In older C++ standards and in C11, definition of the assignment operator text ends with the text:

The evaluations of the operands are unsequenced.

Meaning that side-effects in the operands are unsequenced and therefore definitely undefined behavior if they use the same variable.

This text was simply removed in C++11, leaving it somewhat ambiguous. Is it UB or is it not? This has been clarified in C++17 where they added:

The right operand is sequenced before the left operand.

As a side note, in even older standards, this was all made very clear, example from C99:

The order of evaluation of the operands is unspecified. If an attempt is made to modify the result of an assignment operator or to access it after the next sequence point, the behavior is undefined.

Basically, in C11/C++11, they messed up when they removed this text.


M
M.M

This is further information to the other answers and I'm posting it as the code below is often asked about as well.

The explanation in the other answers is correct and also applies to the following code which is now well-defined (and does not change the stored value of i):

i = i++;

The + 1 is a red herring and it's not really clear why the Standard used it in their examples, although I do recall people arguing on mailing lists prior to C++11 that maybe the + 1 made a difference due to forcing early lvalue conversion on the right-hand side. Certainly none of that applies in C++17 (and probably never applied in any version of C++).