But for my purposes I much prefer to stick something which flags me (exception or logging or whatever) of "this should never happen" instead of crashing. (Undefined.)
I'd rather have solid logging and/or abort handling than some extra yak hairs shaved off the speed on my release builds. I'm weird that way.
An abort would make them obvious, surely unreachable makes them less obvious?
So this is a “hide bugs but maybe improve performance” function.
Compare these two blocks similar to the article
switch (ch) {
case 'a': do_a(); return;
case 'd': do_d(); return;
// ch is guaranteed to be 'a' or 'd' by previous code.
default: assert(0);
}
switch (ch) {
case 'a': do_a(); return;
case 'd': do_d(); return;
default: std::unreachable();
}
If the programmer is wrong about `ch` in the first one, the program terminates.
For the second one, the compiler could change it to be equivalent to if (ch == 'a') { do_a(); }
else { do_d(); }
return;
If the programmer is wrong here, the program might `do_d()` with unintended consequences. I'd say "going down unintended codepaths" is typically considered worse than crashing.N.b. fixed last code example- thanks afiori.
So what does std::unreachable() do here? In this particular case, and with NDEBUG defined and any level of optimization selected, I suspect that, at a minimum, the switch would be replaced as you have shown in all versions - it would take a more complex example to show how std::unreachable() makes a difference. The point is, now we have a choice - and it is one that is being offered without creating any backwards-compatibility issues.
Furthermore, the original function, without assertions, is not guaranteed to crash, with or without std::unreachable(). You need some explicit checks to get a desirable response in the case where a mistake has been made, and that option is just as available whether or not you use std::unreachable().
Therefore, while I agree you have shown that not all broken variants of a given program are equivalent, this does not show that std::unreachable() is harmful.
Instead, the weirdness arises because the compiler makes certain assumptions and transforms the code in ways that depend on them. For example:
1. Null pointers are never dereferenced.
2. Ptr *p is dereferenced.
3. p therefore cannot be null.
4. Ergo, we can omit checking whether p is null.
If the initial premise isn’t actually true (i.e., you slip up and dereferenced a null pointer), the chain of logic breaks down and boom! Applying many such rules can certainly lead to weird emergent behavior—and maybe you should act as if anything can happen—but it’s not a total free-for-all.
The typical usecase would to wrap this in a #define, so that it aborts on a debug build but you get faster code in a production build.
> For example, without the __builtin_unreachable in the example below, the compiler assumes that the inline asm can fall through and prints a “function declared ‘noreturn’ should not return” warning.
void myabort(void) __attribute__((noreturn));
void myabort(void) {
asm("int3");
__builtin_unreachable();
}One example was interesting, though. I could see someone believing these might produce the same optimized assembly (-O2):
void a(int& x, int& y) {
if (&x == &y) __builtin_unreachable();
x ^= y; y ^= x; x ^= y;
}
void b(int& __restrict x, int& __restrict y) {
x ^= y; y ^= x; x ^= y;
}
However, it produces: a(int&, int&): # @a(int&, int&)
mov eax, dword ptr [rdi]
xor eax, dword ptr [rsi]
mov dword ptr [rdi], eax
xor eax, dword ptr [rsi]
mov dword ptr [rsi], eax
xor dword ptr [rdi], eax
ret
b(int&, int&): # @b(int&, int&)
mov eax, dword ptr [rsi]
mov ecx, dword ptr [rdi]
mov dword ptr [rsi], ecx
mov dword ptr [rdi], eax
ret
Is this something compiler contributors would optimize once they know about it? Similar question probably exists with using `__builtin_unreachable` if values aren't aligned versus `__builtin_assume_aligned`.I'd love to see these things illustrated with more real-world examples.
It actually can (Hint: what happens if the operating system `iret`s from its int 3 handler?), although it's probably not a issue in practice. Regardless, you don't need __builtin_unreachable to write:
void myabort(void) __attribute__((noreturn));
void myabort(void) {
asm("int3");
myabort(); // might need `return myabort();` to force TCO,
// but gcc doesn't like that and it should work anyway
}
# Assuming tail-call optimization etcetera, this produces:
myabort:
int3
jmp myabort
which is a correct implementation.However, if you're implementing built-in/standard functions like abort, you presumably know what compiler you're using and don't need a std interface in the first place. There's zero legitimate reason to use a undefined-behaviour-based `unreachable` in application code.
I felt physically ill when I read:
It’s intended to be used when you know you have an execution path in your code that cannot be reached but the compiler cannot figure that out.
It felt like saying to the compiler, "please, find a way to make my program break even more easily". Exactly not what I need.