Ch7.7: Herbceptions at Compile Time
constexpr and consteval code.
Overview
Herbceptions are the only error mechanism in this tutorial, so it should not be a surprise that they work in compile-time code too. There is one extra rule, and it is the one that makes this page useful:
-
A
constexpr/constevalfunction may be declaredthrows, exactly like a runtime one. -
Inside constant evaluation, a herbception behaves like an error value flowing to the
caller — a
try { } catch throws (::std::error e) { }can catch it and the program keeps evaluating. - If nothing catches it, the constant expression is invalid: the compiler emits a diagnostic and compilation fails. A failed expectation becomes a build error, not a runtime surprise.
That last point is the reason this page exists: with herbceptions you can validate constants, tables and algorithms at build time using the same functions your program runs, and get a compile error when a check fails.
1. A constexpr Function That Can Fail
constexpr and throws are independent annotations: the first
says "this may also run at compile time", the second says "this can report an error".
Both together are fine:
#include <fast_io.h>
using namespace ::fast_io::io;
constexpr int checked_div(int a, int b) throws
{
if (b == 0)
{
throw throws ::std::errc::invalid_argument;
}
return a / b;
}
// the happy path is a constant expression
static_assert(checked_div(10, 2) == 5);
int main()
{
println(checked_div(9, 3), "\n"); // also usable at runtime
}
Nothing special was needed: the same function is a compile-time constant expression on valid input and a normal fallible function at runtime.
2. Catching at Compile Time
The handler is allowed during constant evaluation, so a wrapper can decide what a failure means:
constexpr int checked_div(int a, int b) throws
{
if (b == 0)
{
throw throws ::std::errc::invalid_argument;
}
return a / b;
}
constexpr int safe_div(int a, int b) noexcept
{
try
{
return checked_div(a, b);
}
catch throws (::std::error) // recoverable: turn it into a value
{
return -1;
}
}
static_assert(safe_div(10, 2) == 5); // no failure
static_assert(safe_div(1, 0) == -1); // failure, caught at compile time
Both assertions are evaluated by the compiler. safe_div is
noexcept precisely because it absorbs the error — that is the
Ch6.16 rule applied at compile time.
A consteval function works the same way:
consteval int compile_time_div(int a, int b)
{
try
{
return checked_div(a, b);
}
catch throws (::std::error)
{
return -1;
}
}
static_assert(compile_time_div(9, 3) == 3);
static_assert(compile_time_div(9, 0) == -1);
3. An Uncaught Herbception Is a Compile Error
When a herbception escapes a constant expression, the compiler stops. There is no runtime to trap into, so the build fails, and the diagnostic names the throw site:
consteval int must_be_positive(int v) throws
{
if (v <= 0)
{
throw throws ::std::errc::invalid_argument;
}
return v;
}
constexpr int dimensions{must_be_positive(0)}; // ERROR: nothing caught it
error: constexpr variable 'dimensions' must be initialized by a constant expression
note: herbception thrown during constant evaluation was not caught
This is exactly what you want from a validator: a wrong constant in the source is a
build failure at the line that contains it, with the error code you chose. Compare
with a runtime assert: that only fires if the program is run, and only if
it reaches that code path.
// fine: the value is valid
constexpr int width{must_be_positive(80)};
static_assert(width == 80);
4. Build-Time Test Suites
The most valuable use of all this is a compile-time test: a
constexpr bool function that exercises a component and reports through
static_assert. No test framework, no runtime, no build target — a
broken component does not compile.
#include <fast_io.h>
using namespace ::fast_io::io;
constexpr int checked_div(int a, int b) throws
{
if (b == 0)
{
throw throws ::std::errc::invalid_argument;
}
return a / b;
}
constexpr bool self_test() noexcept
{
// 1. the happy paths
try
{
if (checked_div(10, 2) != 5)
{
return false;
}
if (checked_div(-9, 3) != -3)
{
return false;
}
}
catch throws (::std::error)
{
return false; // should not have failed
}
// 2. the failure path, including the exact error it reports
try
{
(void)checked_div(1, 0);
return false; // should have failed
}
catch throws (::std::error e)
{
return e == ::std::errc::invalid_argument;
}
}
static_assert(self_test(), "checked_div is broken");
int main()
{
println("self test passed at compile time\n");
}
Note that self_test is noexcept and catches everything: a
test that can itself fail would not be a compile-time check, it would be a
compile-time crash.
5. Asking the Compiler About Error Contracts
Because the specification is part of the type, it can be queried during compilation. Two tools:
noexcept(expression)— the standard one you met in Ch6.16: "would this call be unable to fail?"-
the herbception traits
::std::is_herbceptions_throws_*— the finer-grained ones, which distinguish "constructible/callable at all" from "constructible/callable through the throws channel".
struct ok_default { ok_default() throws {} };
struct plain { };
// is the default constructor declared 'throws'?
static_assert(::std::has_herbceptions_throws_constructor_v<ok_default>);
static_assert(!::std::has_herbceptions_throws_constructor_v<plain>);
// can we construct it, and can that construction report?
struct from_int { explicit from_int(int) throws {} };
static_assert(::std::is_herbceptions_throws_constructible_v<from_int, int>);
// copies that can report
struct reporting_copy { reporting_copy(reporting_copy const &) throws {} };
static_assert(::std::is_herbceptions_throws_copy_constructible_v<reporting_copy>);
static_assert(!::std::is_herbceptions_throws_copy_constructible_v<plain>);
// callables
constexpr int risky(int) throws { return 0; }
constexpr int pure(int) noexcept { return 0; }
static_assert(::std::is_herbceptions_throws_invocable_v<decltype(risky), int>);
static_assert(!::std::is_herbceptions_throws_invocable_v<decltype(pure), int>);
noexcept(expr) answers the question you
normally have ("must I handle this call?"). The traits exist for generic code that has
to select an implementation depending on whether the operation reports errors
— which is exactly what a freestanding library like fast_io does
internally. You will rarely need them yourself, but you will read them.
6. Why This Works at All
Constant evaluation has no stack unwinding, no heap, no exception objects — and
herbceptions need none of those. The error is a
{domain, code} pair, so the evaluator only has to carry two integers and a
"failed" bit; the domain lookup (::std::error_domain<T>::domain()) is
intercepted during constant evaluation, which is what makes a thrown value a constant
expression instead of a runtime-only object.
The same property holds at runtime, and it is why nothing changes when you move code
between compile time and runtime: the error path of checked_div is
literally a return.
Key Takeaways
-
constexpr/constevalfunctions may bethrows; the two annotations are independent. -
try { } catch throws (::std::error e) { }works during constant evaluation, so a constexpr wrapper can recover from a compile-time failure. -
An uncaught herbception in a constant expression fails the build with
herbception thrown during constant evaluation was not caught— use it to validate constants. -
A
constexpr bool self_test() noexceptplusstatic_assert(self_test())is a real compile-time test suite, using the same functions the program runs. -
noexcept(expr)and::std::is_herbceptions_throws_*let generic code ask how a call can fail before it generates the call. - All of this is possible because a herbception is two machine words, with no unwinding, no allocation, and no exception object.