Check the footnote at item 14, it links to a page which shows one example of how a line with UB can be reached (basically: the optimizer can reorder the code so that parts of the line with UB will run much earlier than you'd expect).
Yes UB in live code can have surprising effects, including executing dead code.
I think the main reason for this is an if(testSomeState()) { //code with UB } block being optimized to just //code with UB.
Functions which are unreachable, maybe I agree, they can't be executed (without UB somewhere in reachable code). I'm not sure that's true though - if you have UB in your hardware (not your code), you can still execute that code. This is why (at least I was taught) never to leave dead code around. There's this better thing for that called source control you may have heard of...
IF you have perfect hardware, and IF you have perfect logic/compiler and IF you have no UB in your program, then maybe you can guarantee that dead code is really dead, but I'm not really sure you want to heavily define dead code like that.
Just remove it. Especially if it has UB in it to cause more fun bugs...
¹or is guaranteed to happen in the future
> Right now, we have the fundamental principle that dead code cannot affect program behavior.
So if you have UB in your future path of execution, the compiler might just do whatever _right now_.
"Anything at all can happen; the Standard imposes no requirements. The program may fail to compile, or it may execute incorrectly (either crashing or silently generating incorrect results), or it may fortuitously do exactly what the programmer intended."
"If any step in a program’s execution has undefined behavior, then the entire execution is without meaning. This is important: it’s not that evaluating (1<<32) has an unpredictable result, but rather that the entire execution of a program that evaluates this expression is meaningless. Also, it’s not that the execution is meaningful up to the point where undefined behavior happens: the bad effects can actually precede the undefined operation."
"If any step in a program’s execution has undefined behavior, then the entire execution is without meaning..."
Yes. So UB is a property of a given execition, not a static property of the program itself.
I don't argue that you can reason about the behavior of any part of the execution, once it evaluates operations with UB.
If a given execution would eventually lead to UB, then it retroactively it cannot be reasoned about.
This time traveling behavior is what is confusing people in thinking that if any execution is UB then all possible executions are UB.
Many people in this thread are confused and think that unreachable UB (either statically or dynamically unreachable) compromises the entire program. This is not true, but your comment helps me better understand how people reached this conclusion.
Reachable UB (either provably reached at compile time or dynamically reached at runtime) can retroactively invalidate the correctness of previous statements. But this does not mean that unreachable UB compromises the correctness of well-defined executions.
int main(int argc, char **argv) {
int a = atoi(argv[1]);
int b = atoi(argv[2]);
return a + b;
}
contains signed integer overflow in some executions but not others. Can you explain what effect the counterfactual signed integer overflow is allowed to have in executions that do not contain signed integer overflow?You are right that once the execution is undefined, you can't reason about the behavior. This includes ordering of side effects, or side effects od operations occurring before the line with UB.
But uttering UB in dead code making execution UB is utter nonsense. Otherwise any execution of any program that just utters __builtin_unreachable() would be undefined.
However, even when processors execute code speculatively, they guarantee that no observable effects of executing that code will happen. So, if you have something like `if false {*NULL=7/0}`, the processor might attempt to execute that line speculatively, but it will revert any change it made, and it will not trap regardless of any other flags.
For example, if a program looks like this:
void foo(int* const p) {
if (p!=NULL) {
bar(p);
}
*p = 1; //this line would be UB if p could be NULL
//so we are free to assume p is not NULL in this scope
}
It will very likely be optimized to this: void foo(int* const p) {
bar(p);
*p = 1;
}
And even if bar() itself had a line checking if p was not NULL, that will get ellided as well if bar() gets inlined.This is a good example of how UB on a later line can have an impact on earlier lines, or even in other places in the code.
Note that it's not executions which trigger UB, it is source code programs that do.
Edit: fixed const pointer syntax.
It is true that when foo() is called with a NULL argument, the UB will be realized before bar() has had a chance to run. But the line with UB has already been proven reachable prior to the actual dereference, so the UB has in a sense already been invoked.
> Note that it's not executions which trigger UB, it is source code programs that do.
For C, I think this interpretation is directly contradicted by DR #109: https://www.open-std.org/jtc1/sc22/wg14/docs/rr/dr_109.html
> Furthermore, if every possible execution of a given program would result in undefined behavior, the given program is not strictly conforming. A conforming implementation must not fail to translate a strictly conforming program simply because some possible execution of that program would result in undefined behavior. Because foo might never be called, the example given must be successfully translated by a conforming implementation.
The point of those numbered lines was to refer to how people normally think about UB. Someone seeing that function may be tempted to say that it's not UB to call with a NULL value as long as the dereference doesn't happen (say, bar() could call exit() or it could run an infinite loop). What you're saying is exactly what the article is saying: the fact that a line with UB is unreachable in practice doesn't mean that it can't affect your program.
> For C, I think this interpretation is directly contradicted by DR #109: https://www.open-std.org/jtc1/sc22/wg14/docs/rr/dr_109.html
The standard talks about executions in the sense of theoretical executions by the C abstract machine, not actual executions of the compiled binary. I thought the other comment was rendering to the latter notion of execution, but perhaps I was wrong and it was also talking about the former.
An infinite loop is UB for precisely this reason: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm So an infinite loop cannot prevent the invocation of UB, because an infinite loop is itself UB.
I would expect that a call to exit() inside bar() would prevent eliding of the NULL check. If the program calls exit() before the dereference, it does not invoke UB and therefore should run correctly. I tried your example and Clang and GCC do not actually elide the NULL check in this case, probably for this reason: https://godbolt.org/z/cnWPz3o59
> The standard talks about executions in the sense of theoretical executions by the C abstract machine, not actual executions of the compiled binary. I thought the other comment was rendering to the latter notion of execution, but perhaps I was wrong and it was also talking about the former.
I don't see how this would change the analysis. Running the binary triggers actual executions of both the abstract machine and the physical binary.
Still gcc doesn't optimize the check away so it is possible there is some other line of reasoning that prevents the transformation.
My first answer was going to be that the transformation is not allowed, but then I thought about it more.
I couldn't get gcc to break it either though. Either it misses the optimization or tries to be conservative with reordering UB with side effects.
I'm afraid it's not nonsense, that's why it's a common misunderstanding ;)
> Right now, we have the fundamental principle that dead code cannot affect program behavior.
Why are people fighting this?
And yes, Rust is much more reasonable about it.
If you are not satisfied, you can read the article's references (there are 2 for unreacheable code). Or any article about UB here for the last 20 years.
Can someone with more knowledge chip in? If I define
void bar(int* const p, int n) {
for (int i=0; i<n; ++i) {
*p = 1
// other code (without UB)
}
}
it might get optimized via loop-invariant code motion to void bar(int* const p, int n) {
*p = 1
for (int i=0; i<n; ++i) {
// other code (without UB)
}
}
so it could still be UB when called with p=NULL and n=0, right?If you had *p == 0 before calling bar(p, 0) then *p had better not be 1 after!
If the compiler has some special knowledge of the target architecture which makes the second form behave as if the first form was executed (such as if NULL is a valid address on the target architecture), it may make the transformation, but that still wouldn't cause undefined behavior, because it must behave as if it had the first form.
For example, imagine a C programmer writes a function that adds two `int` values. Hypothetically, with some inputs, the addition may overflow. That would be undefined behavior, so the compiler assumes the function is never called with such inputs, and optimizes accordingly. The programmer knows this, and assures the function is never called with those inputs. Since the only assumption the compiler made was that the function wasn't called with input which would cause the addition to overflow, and the programmer assured that assumption is correct, the function always behaves correctly.
Consider if the same function's inputs were provided at runtime from stdin. The behavior of the whole program is undefined if invalid inputs are given, but if only valid inputs are given, the program must behave correctly.
So we could say, unreachable UB is fine, but if you do reach it, then it doesn't matter when you reached it.
The implicit assumption is no UB in live code, otherwise the UB in dead code is a red herring.
So the UB is never reached, by the definition of dead code.
Unreachable UB is fine.
If this wasn't the intended message of point 14, then it's phrased wrong.
14. Okay, but if the line with UB is unreachable (dead) code, then it's as if the UB wasn't there.
15. If the line with UB is unreachable code, then the program won't crash because of the UB.
16. If the line with UB is unreachable code, then the program will at least stop running somehow and at some point.
This is nonsense. The line with UB has to be reached for the execution to be undefined.