C++ Logo

std-proposals

Advanced search

[std-proposals] fine-control exception/error/UB handling on function boundary

From: Zamfir Yonchev <zamfir.yonchev_at_[hidden]>
Date: Thu, 27 Aug 2026 19:29:44 +0300
Hi all,

This is not a proposal with any actual code examples but just an
illustration of a general concept.

The problem
As we know C++ has problems with errors: sometimes errors are just ignored
and documented as UB (integer division by 0 for example), sometimes they
are detected and thrown as exceptions but no one is prepared to catch them (
std::bad_alloc for example) and sometimes the behaviour is defined but is
weird and unexpected (unsigned integer underflow for example).
And sometimes error checking is not even desirable because it impedes
performance.
In summary, error cases in C++ are sometimes important to the programmer
but ignored by the compiler and sometimes unimportant to the programmer but
eagerly checked at runtime.

The compiler safeguards can usually be turned off at the translation unit
level, for example by compiling with -fno-exceptions or with NDEBUG to
disable C assertions.
And also sometimes, the standard library (or any other library) may provide
opt-out implementations to avoid unwanted checks (like with using
vec[index] instead of vec.at(index) for example).
But these are case-by-case fixes for a more general problem.

My proposed solution
My proposal is to provide syntax and compiler support for annotating
function signatures with a list of important errors and a list of unwanted
errors that can occur in the body of the function.

Why on the function level?
Functions already form the public API of each program component and error
reporting is best done on the API boundaries. This is already true for
exceptions (a function can be noexcept or potentially throwing). Same for
std::expected - the error is part of the API.
Error-checking annotation on the statement level or on the expression level
is too verbose and on the translation-unit level is too general and not
precise enough.
So, I think functions are the sweet spot.

Errors can be split into 3 categories in terms of how they are handled by
the proposed mechanism:
- implicitly ignored (no checking is done by default);
- explicitly ignored (checking is done by default but can be opt-out)
- always checked.

In the "implicitly ignored" category fall all errors that lead to UB
(out-of-bounds array access, null-pointer dereferencing, signed integer
overflow/underflow, to name a few common examples) and also errors that are
well defined but can lead to unexpected behaviour but still, the compiler
doesn't check them by default (for example unsigned integer underflow,
implicit conversions that cause loss of precision, loss of signedness, etc.)
In the "explicitly ignored" category fall all errors that are rare enough
to ignore them but the compiler still inserts error checking (out-of-memory
exceptions, std::bad_optional_access, std::bad_any_cast,
std::bad_variant_access, etc.)
In the "always checked" category fall the rest of the errors that should
never be ignored. Instead they should be handled (with a try-catch or by
std::expected::value_or, etc.)
It needs to be decided where failures from contracts fall into these
categories (probably in the "always checked" category just to be safe).

Then a function declaration can be annotated with these two lists:
- a list of errors that the function will look out for and report if it
encounters them during execution (called "checked list" for short);
- a list of errors that the function will silently ignore even if it is
theoretically possible to encounter them during execution (called "ignore
list" for short).

The checked list can contain any error type (including exception types and
specific error types which represent the low-level errors
(integer_division_by_0, integer_overflow, loss_of_precision,
implicit_conversion_from_signed_to_unsigned, etc.)
The ignore list can only contain errors that are of the "implicitly
ignored" or the "explicitly ignored" categories.
Both lists are optional, so you can have checked and ignored errors, only
checked errors, only ignored errors or no error annotations.

The mechanism is the following:
- if a function is not annotated with any of these lists it is not affected
by the mechanism (can be overridden by a compiler flag that forces all
functions in a translation unit to be checked)
- otherwise:
- if the compiler encounters an operation that can result in an "implicitly
ignored" error and the error type exists in the checked list, then
additional checks for this operation are added (for example if
std::null_pointer_dereference_access is explicitly checked, then `*ptr`
becomes `__checked(ptr)` where __checked(ptr) is { if(ptr != null) return
*ptr; else throw std::null_pointer_dereference_access{}; } )
- if the compiler encounters an operation that can result in an "explicitly
ignored" error and the error type exists in the ignore list, the checks for
this potential error are removed (for example if std::bad_variant_access is
ignored then `std::get<T>(variant)` becomes a simple cast);
- if the compiler encounters an operation that can result in an "explicitly
ignored" error or an "always checked" error and the error type doesn't
exist in the checked list, the program is ill-formed and diagnostics is
provided;
- in all other cases the compilation continues normally.

Note, that:
- the ignore list is the way to opt-out of unwanted checks enforced by the
compiler otherwise;
- the checked list is for opt-in checks for previously unchecked behaviour
as well as for clarity (what errors does this function allow to propagate
to the callers);
- the checked list can contain error types that the function cannot really
produce. This is to allow forwards-compatibility and more ergonomic generic
code.

Main benefits of this approach
1. It's opt-in, so backwards compatible with existing code. No behaviour or
performance change of the program if the feature is not used.

2. You can reduce UB by raising the safety checks and explicitly telling
the compiler to check cases that were previously unchecked.
Imagine if you can tell the function to do null-pointer dereference checks.
Or to do implicit conversion checks.
Or any kind of sanity checks that were previously only possible by external
tools or compiler-specific flags.

3. You can improve performance by eliminating error checks that the
compiler would otherwise do.
For example, you can tell the compiler to ignore std::bad_alloc exceptions
and then a lot of code could be declared noexcept because allocations in
these places are now error-free (including copy-constructors,
move-constructors, default-constructors, etc. which could have a cascading
effect)
And if you tell the compiler which errors and exceptions you'd like to
ignore then it can prove that certain function calls will never throw and
the exception-handling scaffolding can be optimized away (both inside the
function implementation and outside in the function call).

4. Better function APIs with cleaner exception interface.
If a function throws an exception of a type that is not listed in the
checked list this will be caught at compile-time.
This forces the author of the function to be honest about the kinds of
errors it can throw.

5. It's good for documentation.
Instead of going through the documentation to see what exceptions a library
function can throw you can just check the declaration.

6. Good for code reviews and debugging.
You can "grep" for all the places where you explicitly ignore certain
checks to see if this could be a source of problems.
Of course, this is not an exhaustive search because functions could still
fail or cause UB without any such annotations. But it's a good start.

7. This allows for highly-customizable safety profiles.
For example: "checking for out-of-bounds access is critical but I don't
care about malloc failures"
or: "memory is limited, so memory allocations should be checked but
implicit conversions are part of the design, so no need to check them",
etc.
And these safety profiles can be on a function basis.
Hot code can have a high-safety profile during testing and low-safety
profile during production.
Cold code can remain with a high-safety profile.

Some more details
There's no change on the function ABI (unless the compiler can guarantee
that ABI won't break).

The compiler could also decide to have two versions of a function: one that
can fail and one that can't and choose which one to use depending on what
errors are allowed or ignored on the call site.

Additionally, the compiler could provide a flag, which checks if the
functions are correctly annotated with all the errors they can produce
(except for the implicitly ignored errors) and print warnings for the
non-compliant ones.

Ignoring errors can propagate to lower branches of the call tree. If
function A calls B and it in turn calls C, if you ignore a certain type of
error in A this could result in optimizing the call to C.

The checked and ignore lists could be generated at compile-time using
template metaprogramming, reflection or some other compile-time mechanism,
so they don't have to be hand-written.

Summary
This proposal provides the following benefits:
- low-level optimizations for hot code;
- mitigation mechanisms for UB and unexpected behaviour;
- improved function API and exception interface;
- opt-in in nature for gradual adoption;
- full interoperability with existing features.

The only downside is there's no real-world implementation to test the
feature, yet.

Let me know your thoughts.

Best regards.
-- 
Zamfir Yonchev
Verification engineer at Broadcom Inc.

Received on 2026-08-27 16:30:04