Hello I have a simple question: is modifying an object more than once in the same expression; once through its identifier (name) and second through a reference to it or a pointer that points at it Undefined Behavior?
int i = 1;
std::cout << i << ", " << ++i << std::endl; //1- Error. Undefined Behavior
int& refI = i;
std::cout << i << ", " << ++refI << std::endl; //2- Is this OK?
int* ptrI = &refI; // ptrI point to the referred-to object (&i)
std::cout << i << ", " << ++*ptrI << std::endl; // 3 is this also OK?
In the second it seems to work fine but I am confused about it because from what I've learned; a Reference is just an alias name for an already existing object. and any change to it will affect the reffered-to object. Thus what I see here is that i
and refI
are the same so modifying the same object (i
) more than once here in the same expression.
But Why all the compilers treat statement 2 as a well-defined behavior?
What about statement 3 (ptrI)?
All of them have undefined behavior before C++17 and all have well-defined behavior since C++17.
Note that you are not modifying i
more than once in either example. You are modifying it only with the increment.
However, it is also undefined behavior to have a side effect on one scalar (here the increment of i
) be unsequenced with a value computation (here the left-hand use of i
). Whether the side effect is produced by directly acting on the variable or through a reference or pointer does not matter.
Before C++17, the <<
operator did not imply any sequencing of its operands, so the behavior is undefined in all your examples.
Since C++17, the <<
operator is guaranteed to evaluate its operands from left-to-right. C++17 also extended the sequencing rules for operators to overloaded operators when called with the operator notation. So in all your examples the behavior is well-defined and the left-hand use of i
is evaluated first, before i
's value is incremented.
Note however, that some compilers didn't implement these changes to the evaluation rules very timely, so even if you use the -std=c++17
flag, it might still unfortunately violate the expected behavior with older and current compiler versions.
In addition, at least in the case of GCC, the -Wsequence-point
warning is explicitly documented to warn even for behavior that became well-defined in C++17, to help the user to avoid writing code that would have undefined behavior in C and earlier C++ versions, see GCC documentation.
The compiler is not required (and not able to) diagnose all cases of undefined behavior. In some simple situations it will be able to give you a warning (which you can turn into an error using -Werror
or similar), but in more complex cases it will not. Still, your program will loose any guarantee on its behavior if you have undefined behavior, whether diagnosed or not.
The order of evaluation rules are defined in terms of objects, not references or pointers, or whatever method you take to obtain the object.
That said, your three examples are exactly equivalent in terms of the order of evaluation rules (if we are only considering the object defined as i
).
Hence let's just look at your first example:
std::cout << i << ", " << ++i << std::endl;
For simplicity we can ignore the ", "
and std::endl
, hence:
std::cout << i << ++i;
Since c++ 11 and until c++ 17, this is undefined behavior because the side effects of D (see the graph) is unsequenced relative to the value calculation of C. However, both involves object i
. This is undefined behavior.
Since c++ 17, there is an extra guarantee (on the <<
and >>
expressions) that both the value computation and side effects of C will be sequenced before the value computation and side effects of D (marked by the dotted lines), therefore the code becomes well-defined.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With