This is probably "c++ 101" level question but there is some associated pain so I will ask for comments to be confident.
I have some legacy code to support going back to c++11 and before with this line:
iTmp = ~iTmp++;
This worked 100% until the code was changed to c++17. After changing to c++17 it then calculated a different value, but not always. It depends on where the line is in code whether it executes the 2's complement first or the increment first.
Also, if you try to force with parenthesis it is a compiler error:
iTmp = (~iTmp)++;
error C2105: '++' needs l-value
This makes sense to me because the 2's complement result is not an intermediate variable so the ++ has nothing to operate on.
My research tells me that
Is my evaluation correct?
It is to me a very subtle difference that the previous code is invalid but this line is OK:
if ( ++iTmp > 0)
I did not expect an upgrade to c++17 to cause this subtle error but I had to change the code to the following to solve the problem:
iTmp = ~iTmp;
iTmp++;
This worked 100% until the code was changed to c++17.
It has undefined behavior before C+17, because there are two side effects on iTmp
(one incrementing and one assigning) and neither =
nor the post-increment implied any sequencing between these two side effects.
It seems you just were lucky that the compiler always compiled it in a way that behaves as you wanted it to.
After changing to c++17 it then calculated a different value, but not always.
No, since C++17 it is clearly defined: The right-hand side of the assignment is completely evaluated first, i.e. iTmp
is first incremented. However, iTmp++
produces the previous value of iTmp
and that value computation is also sequenced before the assignment's side effect. Therefore it will behave equivalent to iTmp = ~iTmp
, except that it will also have UB if iTmp
is signed and overflows by the increment.
Refining Expression Evaluation Order for Idiomatic C++
It depends on where the line is in code whether it executes the 2's complement first or the increment first.
I don't know what you mean here. There is no choice anymore for the compiler. Only one order is correct.
Also, if you try to force with parenthesis it is a compiler error:
Yes, because you can't increment a rvalue (which the ~
operator produces).
This makes sense to me because the 2's complement result is not an intermediate variable so the ++ has nothing to operate on.
The term "intermediate variable" does not exist in C++. The reason is that the expression is not a lvalue. That's just a somewhat arbitrary choice of the language specification. It avoids making mistakes.
If (~iTmp)++
was allowed, then it would mean that you first create a new integer object with the value of ~iTmp
, which exists only temporary, then increment that temporary. After the expression the temporary is destroyed again. A behavior like that is likely not intended, because the value of the temporary object won't matter later and if you only wanted to read its value before it is destroyed in the expression then you could have written (~iTmp) + 1
instead, which is much clearer and less confusing.
this is an "order-of-execution" failure.
Yes.
operator-precedencedoes not get involved because both operators are binary.
Operator precedence is relevant because you need to be able to parse that ~iTmp++
is ~(iTmp++)
, not (~iTmp)++
. Note that this is only about parsing. It doesn't affect order of evaluation. I don't know why you think that binary operators somehow don't imply relevance of operator precedence.
two's complement is not an in-place operation so it does not change iTmp but instead returns a value.
"two's complement" is a representation choice of signed integer values in a set of bits. The operator ~
is just the complement and it doesn't matter what the representation choice for the integer type is. Also yes, ~
doesn't modify iTmp
.
the legacy code worked by shear luck - probably due to a compiler bug or mistake which always enforced an operator evaluation order.
Yes, it worked by luck. But the compiler is not to blame at all. The code was simply wrong.
It is to me a very subtle difference that the previous code is invalid but this line is OK:
There is only one modification of iTmp
in that line. It is vitaly important to know and understand the order of evaluation rules in C++ and when they cause undefined behavior, e.g. if two side effects or one side effect and a value computation are unsequenced.
See cpprference for a reference on the order of evaluation rules, although that might be a bit overwhelming as an introduction. Any introductory book to C++ should go over the basic order of evaluation rules in a more approachable manner. Otherwise a newcomer to C++ is eventually going to write dangerous code as in your example.
I did not expect an upgrade to c++17 to cause this subtle error but I had to change the code to the following to solve the problem:
Yes, that's fine and doesn't violate the general rule one should follow: In every expression modify each object at most once.