Originally, I presented a more complicated example, this one was proposed by @n. 'pronouns' m. in a now-deleted answer. But the question became too long, see edit history if you are interested.
Has the following program well-defined behaviour in C++17?
int main()
{
int a=5;
(a += 1) += a;
return a;
}
I believe this expression is well-defined and evaluated like this:
- The right side
a
is evaluated to 5. - There are no side-effects of the right side.
- The left side is evaluated to a reference to
a
,a += 1
is well-defined for sure. - The left-side side-effect is executed, making
a==6
. - The assignment is evaluted, adding 5 to the current value of
a
, making it 11.
The relevant sections of the standard:
An expression X is said to be sequenced before an expression Y if every value computation and every side effect associated with the expression X is sequenced before every value computation and every side effect associated with the expression Y.
[expr.ass]/1 (emphasis mine):
The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand; their result is 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. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.
The wording originally comes from the accepted paper P0145R3.
Now, I feel there is some ambiguity, even contradiction, in this second section.
The right operand is sequenced before the left operand.
Together with the definition of sequenced before strongly implies the ordering of side-effects, yet the previous sentence:
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
only explicitly sequences the assignment after value computation, not their side-effects. Thus allowing this behaviour:
- The right side
a
is evaluated to 5. - The left side is evaluated to a reference of
a
,a += 1
is well-defined for sure. - The assignment is evaluted, adding 5 to the current value of
a
, making it 10. - The left-side side-effect is executed, making
a==11
or maybe even6
if the old values was used even for the side-effect.
But this ordering clearly violates the definition of sequenced before since the side-effects of the left operand happened after the value computation of the right operand. Thus left operand was not sequenced after the right operand which violets the above mentioned sentence. No I done goofed. This is allowed behaviour, right? I.e. the assignment can interleave the right-left evaluation. Or it can be done after both full evaluations.
Running the code gcc outputs 12, clang 11. Furthermore, gcc warns about
<source>: In function 'int main()':
<source>:4:8: warning: operation on 'a' may be undefined [-Wsequence-point]
4 | (a += 1) += a;
| ~~~^~~~~
I am terrible at reading assembly, maybe someone can at least rewrite how gcc got to 12? (a += 1), a+=a
works but that seems extra wrong.
Well, thinking more about it, the right side also does evaluate to a reference to a
, not just to a value 5. So Gcc could still be right, in that case clang could be wrong.
*=
and+=
on the same line? Just break it on two lines. Even it it were sequenced correctly, it would still be unreadable to humans (or increase cognitive load if you want a milder statement) – Drambuie2
andArgs
are evaluated makes no difference. The other important one is rule #8: The side effect (modification of the left argument) of the built-in assignment operator and of all built-in compound assignment operators is sequenced after the value computation (but not the side effects) of both left and right arguments, and is sequenced before the value computation of the assignment expression (that is, before returning the reference to the modified object). – Fledres*=2
returning the reference as the value computation must happen before theref+=...
, yet remains unordered with respect whenres*=2
is truly executed. Thus the increment can still happen before the multiplication? Seems to be in contradiction with #20. Nonetheless, can you make it an answer? – Swimmingly|=
. I haven't written an answer because I'm still not sure what, if anything, is the issue here. – Fled(a += 1) += a;
simply undefined, but I do not see how. – Swimmingly(a += 1) += a;
then remove everything else. Otherwise, make a new question asking about that, and link to this question as the motivation if you want. (Note that the question was already asking for 2 things, an explanation, and a fix. This might be a good way to separate that). – Territusa
" - I think the lvalue-to-rvalue conversion required by [basic.lval]/6 for an operand should be considered a value computation of that operand. Perhaps both the glvalue identity determination and the read access of the lvalue-to-rvalue conversion are two value computations of the same expression? – Fledstatic_assert
is seemingly done in a different order and the results are different from evaluation outside ofstatic_assert
. This is acceptable if behaviour is undefined, however I believe that if clang considers the operations unsequenced, it should warn about it, as it is doing with-std=c++14
(where they are unsequenced). – Yurevstatic_assert()
. – Ensheathe