The top answer is a wrong (but common) misconception:
Undefined behavior is a run-time property*. It CANNOT "time-travel"!
Certain operations are defined (by the standard) to have side-effects and cannot be optimized away. Operations that do I/O or that access volatile
variables fall in this category.
However, there is a caveat: UB can be any behavior, including behavior that undoes previous operations. This can have similar consequences, in some cases, to optimizing out earlier code.
In fact, this is consistent with the quote in the top answer (emphasis mine):
A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input.
However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).
Yes, this quote does say "not even with regard to operations preceding the first undefined operation", but notice that this is specifically about code that is being executed, not merely compiled.
After all, undefined behavior that isn't actually reached doesn't do anything, and for the line containing UB to be actually reached, code that precedes it must execute first!
So yes, once UB is executed, any effects of previous operations become undefined. But until that happens, the execution of the program is well-defined.
Note, however, that all executions of the program that result in this happening can be optimized to equivalent programs, including any that perform previous operations but then un-do their effects. Consequently, preceding code may be optimized away whenever doing so would be equivalent to their effects being undone; otherwise, it can't. See below for an example.
*Note: This is not inconsistent with UB occurring at compile time. If the compiler can indeed prove that UB code will always be executed for all inputs, then UB can extend to compile time. However, this requires knowing that all previous code eventually returns, which is a strong requirement. Again, see below for an example/explanation.
To make this concrete, note that the following code must print foo
and wait for your input regardless of any undefined behavior that follows it:
printf("foo");
getchar();
*(char*)1 = 1;
However, also note that there is no guarantee that foo
will remain on the screen after the UB occurs, or that the character you typed will no longer be in the input buffer; both of these operations can be "undone", which has a similar effect to UB "time-travel".
If the getchar()
line wasn't there, it would be legal for the lines to be optimized away if and only if that would be indistinguishable from outputting foo
and then "un-doing" it.
Whether or not the two would be indistinguishable would depend entirely on the implementation (i.e. on your compiler and standard library). For example, can your printf
block your thread here while waiting for another program to read the output? Or will it return immediately?
If it can block here, then another program can refuse to read its full output, and it may never return, and consequently UB may never actually occur.
If it can return immediately here, then we know it must return, and therefore optimizing it out is entirely indistinguishable from executing it and then un-doing its effects.
Of course, since the compiler knows what behavior is permissible for its particular version of printf
, it can optimize accordingly, and consequently printf
may get optimized out in some cases and not others. But, again, the justification is that this would be indistinguishable from the UB un-doing previous operations, not that the previous code is "poisoned" because of UB.
a
isn't used (except for calculating itself) and simply removea
– 4386427