Document number: | P3541R0 | |
---|---|---|
Date: | 2024-12-16 | |
Audience: | SG21, SG23, EWG | |
Reply-to: | Andrzej Krzemieński <akrzemi1 at gmail dot com> |
noexcept
In this paper we point out that a number of correctness-checking features
currently considered for addition to C++ that allow responding
to runtime-detected violations via exceptions, should
define the interaction with the noexcept
-specifier and the
noexcept
-operator.
There is a number of proposals currently on the flight aiming at detecting at run-time the incorrect usages of the language or libraries, and responding to them in a number of ways, including by throwing an exception. These proposals include:
In all these proposals we have the notion of a violation handler: a user-customisable function that is invoked in response to a bug detected at run-time at the point in program where the bug has been detected. One of the intended use cases for these handlers is for the programmer to be able to throw an exception that would on the one hand prevent the execution of the following expression, and on the other resume the program execution from another location.
The effect of these added runtime-checks is that expressions and constructs that used to be visibly non-throwing now can throw exceptions, possibly only under some compiler configurations. Consider the following function.
Tool::~Tool() // noexcept by default { for (int i = 0; i <= size(); ++i) { static_assert(noexcept(_vector[i])); _vector[i].retire(i); } }
In C++23 this definition of a destructor compiles fine: the static assertion does not fire. But if a
bounds
profile is enforced, as per
[P3081R0],
and the programmer installs a throwing violation handler, the element-accessing
expression can now throw an exception. So the question is, what should the
noexcept
-operator return in this case, and if the answer should depend on
whether the bounds
profile is enforced in the given context. Note also
that even if the runtime check and the throw are performed outside of the expression,
as suggested in
[P3081R0],
they are still performed inside the enclosing destructor, which is implicitly declared
noexcept
, so an exception that is thrown doesn't achieve the goal of not
terminating the program and still ends up calling std::terminate()
.
In short, the idea of throwing an exception everywhere where we would previously have
undefined behavior is clearly at odds with the semantics of noexcept
,
and also at odds with the notion of exception safety guarantees ([ABRAHAMS]).
While it is true that upon undefined behavior the language imposes nothing on the programs,
and they are free to lie about noexcept
, this is not helpful
when the goal is to guarantee a predictable, non-surprising, non-UB, non-terminating behavior
of an incorrect program.
Contracts ([P2900R11])
are special in this regard, as the runtime-checks are never added implicitly
but require an explicit usage of either pre
or
post
on function declarations or contract_assert
as a
statement.
[P2900R11]
clearly defines the interaction with noexcept
.
Preconditions and postconditions, even if physically checked in the caller,
interact with noexcept
as if they were evaluated inside the function body.
contract_assert
is a statement rather than an expression, so that its
non-throwing property cannot be tested in any way.
However, while this works for now, we know of two features that propose to be able to test the non-throwing property of arbitrary statements:
throw(auto)
which will deduce the exception specification from all
statements in the function body,
do
expressions, which turn a sequence of statements into an
expression which you can inspect with the noexcept
-operator.
If either of these features is added, the question will need to be answered:
is contract_assert
potentially throwing?
Given that the same idea — potentially throwing in a controlled way from every place that is undefined-behavior-like — surfaces in a number of proposals, the C++ community would benefit from a clear design direction in this respect.
The following are the possible directions that we can think of.
false
is potentially throwing and the throwing behavior can be controlled by programmers.
noexcept(true)
but can throw nonetheless.
Any of the choices comes with a cost. #1 goes against the intuition of exception safety,
where you need some operations to be never-throwing in order to provide a strong (transactional)
exception safety guarantees. It also goes against the direction outlined in
[P0709R4].
Note that a goal of turning UB into a throw requires exception safety to still be provided. #2 closes the door for the direction, where any bug could be turned into an exception
and recovered from at some higher levels. #3 has a consequence of a correct program
(even if it has no bugs and does no recovery) taking different paths of execution depending
on the (conformant) compiler switches. #4, while allows throwing, makes the environment —
the compiler and the libraries that depend on noexcept
— unprepared for
handling the exception in a reliable way, so the goal of reliably recovering from a detected bug.
Options #1 and #2, while they close doors for certain use cases, have a clear, simple, teachable model
to offer.
do
expressions"