1. Revision history
1.1. R1
-
Changed title from "Split constexpr
into distinct runtime and consteval functions."uncaught_exceptions -
Added proposed changes to
.current_exception -
Added design considerations.
2. Background
[P3068] applied the
specifier to the functions in the header
.
and
are among these functions, however at runtime their return values depend on mutable global state.
It is impossible for these functions to always return a constexpr value that is consistent with the runtime state.
When evaluated in a constant expression, in current implementations of [P3068]
returns the number of uncaught exceptions in the current constant evaluation instead of the current thread.
returns a null exception pointer if there is no currently handled exception in the current constant evaluation.
The inconsistency of the semantics of these functions depending on whether they are called from a constant expression or at runtime causes a breaking change when the functions are called in trial constant evaluation.
integer variables are usable in constant expressions when they are constant-initialized, so when initializing them the initializer is constant evaluated first, and if that fails then the variable is initialized at runtime. Consider:
#include <exception>int i = 1 ; int main () { try { struct S { ~ S () { // x is initialized to 0 in trial constant evaluation const int x = std :: uncaught_exceptions (); i = x ; } }; S s ; throw 0 ; } catch (...) { // b is initialized to false in trial constant evaluation const bool b = ( std :: current_exception () != nullptr ); return i + b ; } }
The program returns 0, which is a breaking change compared to C++23, where it returns 2.
3. Proposal
-
Remove the
specifier fromconstexpr
.uncaught_exceptions () -
Add a
function with similar semantics toconsteval int consteval_uncaught_exceptions () noexcept
, but counting exceptions within the current constant evaluation.uncaught_exceptions () -
Make
only constant when the currently handled exception was thrown in the same constant evaluation.current_exception () -
Add
function with similar semantics toconsteval int consteval_current_exception () noexcept
, but only returning pointers to currently handled exceptions that were thrown in the current constant evaluation, otherwise returning null.current_exception ()
4. Motivation
is useful to write scope guards and other patterns to conditionally execute code when normally exiting a scope or due to stack unwinding.
Now that we have exception facilities available in constant evaluation, this functionality is similarly useful in that context.
Putting the
functionality in a separate
function makes this functionality available in constant evaluation without a breaking change.
is also generally useful around exception handling, whether at runtime or in constant expressions.
The breaking change involving
can be avoided by making it conditionally constant.
The resulting function is still generally useful in many exception handling use cases.
Putting the remaining
functionality in a separate
function serves the remaining use cases.
5. Design considerations
5.1. Definitions
Semantically, evaluation of initializers of
variables are sequenced normally on the same thread.
I define the terms currently handled exception and uncaught exceptions so that they do not depend on whether the current point in execution is within a constant evaluation or at runtime.
They both refer to the exception state in the current thread.
These definitions help to define the
interfaces to be consistent across compile time and runtime.
int main () { try { struct S { ~ S () { // there is one uncaught exception here constexpr int a = [] { //there is still the one uncaught exception here return 0 ; }(); } }; S s ; throw std :: runtime_error {}; } catch ( const std :: runtime_error & e ) { // The currently handled exception is a runtime_error object constexpr int b = [] { // The currently handled exception is still the same runtime_error object return 0 ; }(); } }
5.2. Consistent runtime and compile time behavior
functions preferably have the property that they return consistent results at runtime and at compile time.
This property makes it easier to reason about their behavior.
One standard library function that does not have this property is
.
This function is explicitly designed to return different results within and outside of constant expressions.
Despite this, it is still a source of confusion, even outside of trial constant evaluations.
The danger of confusing users is even higher for functions that are not designed to have different semantics at runtime and at compile time, and the difference is subtle.
5.3. Keep constexpr
where we can
We should allow constant evaluation where we can be consistent with the runtime behavior.
This is impossible for
, but it is possible for
in some cases.
In particular when
is evaluated within a catch handler that is still in the same constant expression.
Example:
#include <exception>// MAYBE_CONSTEXPR either expands to "constexpr" or nothing int main () { MAYBE_CONSTEXPR int x = []{ try { throw 42 ; } catch (...) { // the currently handled exception is an int object with value 42 return * std :: exception_ptr_cast < int > ( std :: current_exception ()); } }(); return x ; // returns 42; }
5.4. Keep useful functionality
It is worthwhile to introduce
functions with different names with subtly different semantics to the corresponding runtime functions.
This fills the gap of
functionality where we cannot keep the runtime and compile time behaviors consistent.
5.5. Naming
The newly introduced functions are
and
.
The
in their name refer to the subtly different semantics to their runtime counterpart:
the return values of these functions correspond to the current constant evaluation, not the current thread.
6. Implementation experience
There is previous implementation experience for [P3068] including these two functions.
Splitting
into separate runtime and
functions is a simple change.
Implementing
is similarly simple.
A conditionally constant
can be implemented on top of the [P3068] implementation:
inline void nonconst () noexcept {} constexpr exception_ptr current_exception () noexcept { exception_ptr ret = current_exception_P3068 (); if ( ret == nullptr ) { nonconst (); } return ret ; }
7. Wording
Wording is relative to [N5013].
7.1. [exception.syn]
constexprint uncaught_exceptions() noexcept; consteval int consteval_uncaught_exceptions() noexcept;
…
constexpr exception_ptr current_exception() noexcept; consteval exception_ptr consteval_current_exception() noexcept;
7.2. [uncaught.exceptions]
constexprint uncaught_exceptions() noexcept;
Returns: The number of uncaught exceptions ([except.throw]) in the current thread.
Remarks: When
, throwing an exception can result in a call of the function
.
consteval int consteval_uncaught_exceptions() noexcept;Returns: The number of uncaught exceptions ([except.throw]) in the current constant evaluation. Remarks: When
consteval_uncaught_exceptions () > 0
, throwing an exception can result in a call of the function std :: terminate
.
7.3. [propagation]
constexpr exception_ptr current_exception() noexcept;Constant When: There is a currently handled exception that was originally thrown within the current constant evaluation.
…
consteval exception_ptr consteval_current_exception() noexcept;Returns: A null
exception_ptr
if there is no currently handled exception that was originally thrown within the current constant evaluation, otherwise an exception_ptr
object that refers to the currently handled exception or a copy of the currently handled exception.
The referenced object shall remain valid at least as long as there is an exception_ptr
object that refers to it.
If the function needs to allocate memory and the attempt fails, it returns an exception_ptr
object that refers to an instance of bad_alloc
.
It is unspecified whether the return values of two successive calls to consteval_current_exception
refer to the same exception object.
If the attempt to copy the current exception object throws an exception, the function returns an
object that refers to the thrown exception or, if this is not possible, to an instance of
.