You can come up with specific cases and decide that they should be handled a different way. You're absolutely right that this NULL check elimination should generate a warning. But it's really really hard to come up with a general algorithm that correctly differentiates between "your code implies that this check can never be true, watch out!" and "your code implies that this check can never be true, so we correctly removed it during optimization".
For a trivial NULL-related example, the standard C free() function does a NULL check on its parameter. free(NULL) is legal and does nothing. A naive "does this check for NULL after dereferencing the pointer?" checker would therefore warn for this code:
printf("the pointer's value is %d", *p);
free(p);
To a human, this obviously shouldn't be warned about, while the other example should be. But how does the computer tell them apart? It's hard.
Super, super pedantic point: that probably wouldn't happen with your example because most of the time, free is defined in a shared library somewhere else, and the compiler wouldn't be able to inspect its code. Even if it's in the same source file, most compilers don't optimize across non-inlined function boundaries.
But! You made a good point, and it would apply to a function that did a null-check which was inlined. It's easy for us to imagine a function which 1) does a null-check, 2) gets inlined, and 3) is used in places in the code which dereference the pointer before calling the function.
While compilers won't optimise functions they don't yet know about, they do know the standard library and the respective guarantees and constraints. This includes removing calls to memset for things that are never read again (horrible for passwords or keys in memory, which is why there is a SecureZeroMemory or related function in operating systems) or other things.
Great example, however this does not apply to free - or I would be very surprised if it did. It is relatively common for people to override malloc and free at runtime, so I would be very surprised if the compiler treated malloc or free as a compiler intrinsic, and inlined the code. I would not be surprised, however, if the compiler used the semantics of malloc or free to reason about the surrounding code. The original point, however, was about inlined code leading to generated code that no reasonable person would write (null check after use). So I still think that would not happen for free.
I just tested clang, and free(malloc(42)); gets completely optimized out, as does free(NULL);. free(argv) doesn't, so it's not quite that clever, at least.
The standard library thing is interesting. Modern compilers actually understand that stuff a lot. I don't think they'll reach into the implementation, but they understand the guaranteed semantics. For example, they know that this is undefined behavior:
free(ptr);
printf("after free, it contains %d\n", *ptr);
And the nasal demons shall flow freely. The compiler will certainly know that there's a NULL check in there. However, the smarts are different enough that it will also know not to warn you about it. So, yes, this is an example that won't happen in reality, although there's no reason it couldn't.
Sorry, I don't understand what's so hard about this problem? Why not just emit a warning when the compiler exploits undefined behavior to make some line of code unreachable. By "line of code" I mean code that's written by the user, not code after macroexpansions, inlinings or whatever. So the warning would mean that either you have a bug, or you can safely delete some code. Both of these are helpful.
Technically the compiler doesn't exploit the undefined behaviour. It exploits the assumption that it cannot happen and thus it's free to assume everywhere that only defined behaviour happens. Which means, the optimisations are for optimising the defined cases with no regard at all to the undefined behaviour.
You'll notice in a lot of cases that the exploitation of UB looks different for the same cases with different compilers or even compiler versions. This is because the compiler doesn't see »Oh, UB, I can optimise that« but rather »In this case I can do this which remains valid for all defined cases«.
Also, as others have pointed out, even if the compiler would emit a warning, it would be way too much noise because such things happen all the time.
> Also, as others have pointed out, even if the compiler would emit a warning, it would be way too much noise because such things happen all the time.
How so? For example, this code:
printf("the pointer's value is %d", *p);
free(p);
would not cause a warning under my proposal, even if free() contains a NULL check. The source code contains no unreachable lines, only the inlined/macroexpanded code does. On the other hand, most "gotcha" examples proposed so far do have unreachable source lines, and would lead to warnings.
Can you give an example of useful code that contains unreachable lines before macroexpansion and inlining? What's wrong with emitting a warning so the programmer can delete the useless line?
> You'll notice in a lot of cases that the exploitation of UB looks different for the same cases with different compilers or even compiler versions.
That's OK. The problem is with each individual compiler deleting code without warning. If compiler X deletes a line of my code, then it should warn me about it. If compiler Y doesn't delete that line, it doesn't have to warn me.
For a trivial NULL-related example, the standard C free() function does a NULL check on its parameter. free(NULL) is legal and does nothing. A naive "does this check for NULL after dereferencing the pointer?" checker would therefore warn for this code:
To a human, this obviously shouldn't be warned about, while the other example should be. But how does the computer tell them apart? It's hard.