Introduction
Both C23 and C++23 added the unreachable()
and std::unreachable()
functions, respectively, that insert undefined behavior into your program. (If you don’t know what undefined behavior is, you should definitely read that article first.)
Ordinarily, undefined behavior should be avoided at all costs. So why does unreachable()
exist and when would you ever want to use it? Its name gives a hint: it tells both compilers and programmers that the line of code on which unreachable
is “called” is never actually called. What use is that? It allows you to:
- Suppress warnings.
- Perform a bit of optimization.
It can do those things because the compiler is allowed to assume that undefined behavior never happens.
Suppressing Warnings
It’s probably easiest to explain how unreachable()
can be used to suppress warnings by way of example. The C Exception library includes the following:
typedef void (*cx_terminate_handler_t)( cx_exception_t const* );
static cx_terminate_handler_t cx_impl_terminate_handler;
That is, it declares a pointer-to-function type for a “terminate handler” and a global such pointer allowing the library user to install a custom function to be called upon encountering an unrecoverable situation, e.g., invoking throw
without any active try
. The library calls the handler via:
[[noreturn]] static void cx_terminate( void ) {
(*cx_impl_terminate_handler)( &cx_impl_exception );
unreachable();
}
If unreachable()
were not there, you’d get a warning like:
warning: function declared 'noreturn' should not return
The contract is that the terminate handler must terminate the program, i.e., not return. Hence, cx_terminate
is declared [[noreturn]]
. The problem is that [[noreturn]]
can’t be part of a typedef
, hence the compiler has no way to know that a function called via pointer-to-function won’t return. By inserting the unreachable()
, you’re explicitly telling the compiler that code after the function is called is unreachable, hence cx_terminate
will not return, hence there is no reason to warn.
A similar example is:
bool cx_impl_try_condition( cx_impl_try_block_t *tb ) {
switch ( tb->state ) {
case CX_IMPL_INIT:
// ...
return true;
case CX_IMPL_CAUGHT:
// ...
[[fallthrough]];
case CX_IMPL_TRY:
case CX_IMPL_THROWN:
// ...
return true;
case CX_IMPL_FINALLY:
// ...
return false;
}
unreachable();
}
The code for the individual cases isn’t important here; what is important is that the code switches on an enumeration, has a case
for every enumeration constant, and every case ends with return
.
If unreachable()
were not there, some compilers will give you a warning like:
warning: control reaches end of non-void function
Such a compiler is being (overly?) cautious and considering the possibility that tb->state
might contain a value other than one of the declared enumeration constants, hence none of the cases will match, and the function will “fall out the bottom” and return without returning a value. By inserting the unreachable()
, you’re reassuring the compiler that this can’t happen.
Note that it’s better to put
unreachable()
after theswitch
and not in adefault
case since adefault
prevents the compiler from catching unhandled enumeration constants.
Optimization
Similarly, it’s probably easiest to explain how unreachable()
can be used to perform (small) optimizations by way of example. The cdecl program includes the following:
static bool c_ast_check_alignas( c_ast_t const *ast ) {
if ( ast->align.kind == C_ALIGNAS_NONE )
return true;
// ... lots of code ...
switch ( ast->align.kind ) {
case C_ALIGNAS_NONE:
unreachable();
case C_ALIGNAS_BYTES:
if ( !is_01_bit( ast->align.bytes ) ) {
print_error( &ast->align.loc,
"\"%u\": alignment must be a power of 2\n",
ast->align.bytes
);
return false;
}
break;
case C_ALIGNAS_SNAME:
// nothing to do
break;
case C_ALIGNAS_TYPE:
return c_ast_check( ast->align.type_ast );
} // switch
return true;
}
Briefly, the function checks that the semantics of an alignas
are valid. The first if
checks whether a declaration actually contains an alignas
: if not, it returns immediately avoiding the “lots of code.” However, later on, a switch
is done on the kind of alignment. Since C_ALIGNAS_NONE
has already been checked for by the if
, there’s no reason to include a case
for it — except omitting it would cause the compiler to give you a warning like:
warning: enumeration value 'C_ALIGNAS_NONE' not handled in switch
If unreachable
didn’t exist, you could instead do either of the following to suppress the warning:
case C_ALIGNAS_NONE:
abort();
or:
case C_ALIGNAS_NONE:
break;
The first (the comparison to C_ALIGNAS_NONE
and the abort()
) will still generate code to do the comparison and call abort
— code that will never be executed.
The second (the comparison to C_ALIGNAS_NONE
and the break
) will both be optimized away by the compiler (which is good), but it doesn’t convey to programmers that C_ALIGNAS_NONE
has already been accounted for.
Using unreachable()
here is better than either of those alternatives since it generates no code and also conveys to programmers that the case is impossible.
Conclusion
In a few corner cases, unreachable
is useful to tell both compilers and programmers that a particular code path is unreachable. For the compiler, this can suppress warnings and also optimize away code.
Top comments (4)
Yep, but what's wrong with just
for(;;){}
. Works fine with C++17.It would work, sure, at least in the examples I've given. If you're someone in charge of implementing the standard C or C++ libraries, you might even do:
In programs, using
unreachable()
expresses your intent much better than doingfor(;;){}
explicitly.However, the
for
implementation doesn't actually generate undefined behavior — which is what you want. Both gcc and msvc implement their own functions to generate undefined behavior, e.g.,__builtin_unreachable()
. Presumably, they wouldn't have gone to the trouble of implementing those functions unless it was better.One example I found where it actually makes a small difference is given by this answer. If you replace the
unreachable()
withfor(;;){}
, you get different branch ordering and prediction. I've confirmed this.In your codebase, if you want
unreachable
in C++17, you can always implement your own using the possible implementation given here.In C++23 and earlier it does.
Not sure about C++26. There is a paper, (isocpp.org/files/papers/P2809R3.html), whose title seems to indicate that it will no longer be UB in C++26. Which if so is sad, yet another case where the committee chooses to make things more complex and unreliable. :(
Instead of writing up an explanation of the UB I just quote the Google AI synopsis I got when I searched for the standard's wording - which in C++23 is in §intro.progress:
Ah, OK: C and C++ diverge on how infinite loops are handled.
That aside, the point is to use
unreachable
regardless of how it's defined under the hood. If you want to define it usingfor
, go ahead; but there are alternatives.