The solution is to avoid UB. std::unreachable() is just an easy-to-invoke way of hitting UB, mentioned for the sake of the discussion. They did later take your advice “Let’s say we change std::unreachable() to int* ptr = nullptr; *ptr = 99; in the previous example.”. They still got improper codegen out of the compiler.
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.
> it would be better if programmers read deliberate instances of UB as if they literally said "std::unreachable()"
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.
> There's no standard terminology I'm aware of to distinguish the two.
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.)
unreachable is for when you are a certain that some code cannot be reached, so that it's okay to translate and optimize it under that assumption.
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.
Author here.
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
im not an c++ expert, but it looks like the unreachable macro of rust.
In rust it allows optimizing the generated binary to not care about special paths.
If an integer can only be 1-4, then it doesn't make sense to dedicate an code path for 0 or 5+ that will never be called.
in an explicit for loop its easy to know for the compiler, but when this information crosses boundaries like function calls then it can make it easier and can reduce the binary size.
To elaborate on the other reply to your comment: In Rust, if you don't use `unsafe` and still manage to trigger UB, then that's a bug in the compiler or the library code you called (which in turn has to have used unsafe somewhere to trigger UB).
So the fact that `unreachable!()` does not require the use of the keyword `unsafe` means that it can't be like C++ `std::unreachable()`.