Attention people who think UB is fine
Posted by signa11 10 hours ago
Comments
Comment by omoikane 8 hours ago
Another alternative is to use `__builtin_unreachable()` (not standard, but available in GCC and Clang).
Comment by thomashabets2 20 minutes ago
Yes, into the shame cube I go. It doesn't change anything, and it applies equally to C and C++, but you are correct. Thanks.
Comment by zarakshR 8 hours ago
Comment by munch117 2 hours ago
Comment by thomashabets2 25 minutes ago
Comment by charleslmunger 8 hours ago
Comment by thomashabets2 29 minutes ago
But then it's not UB, and not the topic at hand. So I wouldn't call it "sort of true" for a statement about UB, because it doesn't apply to not-UB.
It's also different in a consteval context. But that doesn't make it less true outside consteval context.
Comment by mirashii 6 hours ago
Comment by Vaushite 8 hours ago
You saw it yourself: unreachable generates no code and suppresses the function epilog (duh - it's unreachable) but null deref generated a trap instruction in GCC. If you wanted a trap instruction, maybe this compiler reliably generates one from a hardcoded null deref.
Comment by thomashabets2 37 minutes ago
> "the definition of std::unreachable is: invokes undefined behaviour" does not mean that all undefined behaviour is equivalent to calling std::unreachable.
Well, obviously not. The definition of UB is that anything can happen, so by transitive property it means that std::unreachable can trigger any behavior.
That's the whole point of UB, and the point of my post: It's UB! You cannot reason about what will happen! The whole point of the post is "No you CANNOT reason about what will happen". So of course I am making no claims about what will happen, just three interesting observations about what did in fact happen when I happened to try it, to prove this point.
> If you wanted a trap instruction, maybe this compiler reliably generates one from a hardcoded null deref.
No, even in the examples in my post I show that it's not reliable. It's UB!
Hell, we get very different output just between -O1 (https://godbolt.org/z/33e1nGE38) and -O2 (https://godbolt.org/z/3coTqP31W).
Comment by nextaccountic 5 hours ago
When you say "this UB does ... (like generates a trap instruction)" you are being hyper specific for a given compiler version and platform and optimization level and any other knob. It may not stay the same after you update the compiler. And what happens a lot is that newer, better optimizations (such as more opportunity to autovectorization), which attract people to newer compiler versions, may exploit UB in more creative ways.
For example, to exploit autovectorization, compilers may want to push some test out of a loop. If this test contains UB, this optimization will change where the UB is triggered. So that trap instruction may happen at a different place in the code, depending on optimizations.
Comment by pestatije 8 hours ago
Comment by johnea 9 hours ago
Comment by thomashabets2 16 minutes ago
No. That's just one of the MANY ways to trigger UB. In the article I also trigger UB by dereferencing a null pointer instead.
In fact, here's an example where calling `unreachable()` and dereferencing a null pointer produce the EXACT same assembly: https://godbolt.org/z/538qfj9z9
Comment by mpyne 8 hours ago
Though, it would be better if programmers read deliberate instances of UB as if they literally said "std::unreachable()" at that point in the code, as that would better convey how the optimizer passes will see things after dozens of frontend and middle-end passes have run before it.
Comment by torstenvl 8 hours ago
Except that isn't true.
There are basically two kinds of undefined behavior.
The first is undefinable behavior, the kind that easily goes disastrously wrong, like writing to random memory locations.
The second is incidentally undefined behavior, something where there just isn't enough implementation consensus for the standard to settle on it, like realloc(p, 0).
There's no standard terminology I'm aware of to distinguish the two. But the distinction exists.
And where programmers read deliberate instances of UB, it's usually the latter, e.g., writing directly to an address memory mapped to device I/O in that particular environment.
Comment by NobodyNada 7 hours ago
There is: implementation-defined behavior. Your example of writing to an MMIO address is such an example, as the standard says casting an int to a pointer is implementation-defined. (However, the C standard has plenty of cases of undefined behavior that probably should have been implementation-defined.)
Comment by kazinator 7 hours ago
If you think a point in the program should not be reached unless there is a bug somewhere, you can write abort() rather than unreachable(), among other possibilities.
abort() doesn't return; any code after it is actually unreachable; we can confidently write
abort();
unreachable(); // redundant
The compiler can already assume that abort() doesn't return, and so anything after it is unreachable. But it may not assume that abort() itself is not reached.So in essence we can get some of the same optimizations, somewhat blunted, so to speak.
For instance, if we put unreachable() at the end of a function, the compiler can omit generating the code for returning from the function; if that point is reached, the instruction pointer will wander off into whatever instructions occur after that.
If we put abort() at the end of a function, the compiler can likewise omit generating the code for returning from the function. But the instruction pointer won't wander off: there will be a call or jump to abort. So, not quite as optimized in size, but optimized still.
Comment by stop50 8 hours ago
Comment by thomashabets2 6 minutes ago
So the fact that `unreachable!()` does not require the use of the keyword `unsafe` means that it can't be like C++ `std::unreachable()`.
Comment by tyushk 8 hours ago
[1] https://doc.rust-lang.org/std/macro.unreachable.html [2] https://doc.rust-lang.org/std/hint/fn.unreachable_unchecked....