1. Changelog
1.1. r0 -> r1
-
Added handle_or_rethrow()
-
Added EWG poll from Rapperswil
-
Fixed minor typos and clarified a few points
2. Introduction
is a weird beast. Unlike the other
types it is completely type erased,
similar to a hypothetical
. Presumably because of this, it is the only
type to
offer no
method, since it wouldn’t know what type to return. You might even say it should
take [N4282]'s title as "The World’s Dumbest Smart Pointer" since the only way to dereference it
is by throwing!
This proposal suggests adding methods to
to directly access the pointed-to
exception in a type-safe manner. On standard libraries that implement
by
direct construction rather than using
/
/
/
(currently MS-STL
and libstdc++, but not libc++), it should now be possible to both create and consume a
with full fidelity even with exceptions disabled. This should ease
interoperability between codebases and libraries that choose to use exceptions and those that do
not.
I have a proof of implementability without ABI breakage here. It
is an implementation of the proposed methods for both MSVC and libstdc++. On Linux, it is 385 times faster than using
and
as is needed today. On Windows, it is only 10 times faster due to needing to trap into the kernel to call DecodePointer().
I haven’t tested on libc++ or with the special ARM EH ABI, but based on my reading of those
implementations, the same strategy should work fine. Pull requests welcome!
3. Polls
3.1. EWG Rapperswil 2018
EWG reviewed the paper for the general concept and how it interacts with exceptions, but not the specific API proposed here. This is reflected in the poll wording.
Does EWG want a non throwing mechanism to get to the exception held by
even if the
performance was the same
SF | F | N | A | SA |
---|---|---|---|---|
16 | 8 | 1 | 0 | 0 |
4. Proposal
This is an informal description of the methods I propose adding to
. I don’t have
fully fleshed out proposed wording yet.
All pointers returned by this API are valid until this
is destroyed or assigned
over. Copying or moving the
will not extend the validity of the pointers. (The
intention is that they should point directly to the exception object, or a base subobject, owned by
the
, so they should have the same validity as that object’s lifetime.)
Calling any of these methods on a null
is UB.
4.1. High-level API
4.1.1. handle()
template < typename ... Handlers > /*see below*/ handle ( Handlers && ... handlers ) const ; bool handle () const { return false; }
Handles the contained exception as if there were a sequence of
blocks that catch the
argument type of each handler. The argument type is determined in a similar way to the
template deduction
guide to detect the
first and only argument type of a Callable object. However, it must also detect an
callable
as the natural analog of the
catch-all. If present, the catch-all must be the last
handler.
The return type of
is the natural meaning of combining the return types from all handlers
and making it optional to express nothing being caught. More formally:
using CommonReturnType = common_type_t < result_of_t < Handlers > ... > ; using ReturnType = conditional_t < is_void_v < CommonReturnType > , bool , optional < CommonReturnType >> ;
If none of the handlers match the contained exception type, the call to
returns either false
or an empty
. If any handler matches, it returns either true
or an
constructed from the handler’s return value.
This method does not "activate" the exception, so it will not be available to
or
statements inside of the handlers.
This API is inspired by folly::exception_wrapper. It can be implemented in the standard today, albeit without the performance benefits. Additionally, I think the valid lifetimes of the references would be shorter than is proposed here.
4.1.2. handle_or_rethrow()
template < typename ... Handlers > common_type_t < result_of_t < Handlers > ... > handle_or_terminate ( Handlers && ... handlers ) const ;
Similar to handle(), but calls
if no handler matches the current
exception. Unwraps the return type since if it returns, a handler must have matched.
4.1.3. handle_or_terminate()
template < typename ... Handlers > common_type_t < result_of_t < Handlers > ... > handle_or_terminate ( Handlers && ... handlers ) const ;
Similar to handle(), but calls terminate_with_active() if no handler matches the current exception. Unwraps the return type since if it returns, a handler must have matched.
4.1.4. try_catch()
template < typename T > requires is_reference_v < T > add_pointer_t < T > try_catch () const ; template < typename T > requires is_pointer_v < T > optional < T > try_catch () const ;
If the contained exception is catchable by a
block, returns either a pointer to the
exception if
is a reference or an
containing the caught pointer if
is a pointer.
If the
block would not catch the exception, returns
/
.
The pointer case is a bit odd and throwing/catching pointer is fairly rare so it could use some
explanation for why it is both different and the same as the reference case. They have different
return types because returning
is impossible since the exception holds a
and
may
be
, and there is no
object to return a pointer to (
). Returning
/
in this case would also be incorrect because there would be no way to distinguish a thrown null
pointer from a type mismatch. Luckily,
has the same access API as
so even though
the return types of these functions are different, consumers can treat them the same:
if ( auto ex = ex_ptr . try_catch < CatchT > ()) { use ( * ex ); }
Using today’s API this would be:
try { rethrow_exception ( ex_ptr ); } catch ( CatchT & ex ) { use ( ex ); } catch (...) { // ignore }
4.1.5. terminate_with_active()
[[ noreturn ]] void terminate_with_active () const noexcept ;
Equivalent to:
try { rethrow_exception ( * this ); } catch (...) { terminate (); }
Invokes the terminate handler with the contained exception active to allow it to provide useful information for debugging.
Note: I believe this definition is exactly equivalent to just calling
inside a
context.
4.2. Low-level API
This is the low-level API that is intended for library authors building their own high-level API, rather than direct use by end users.
4.2.1. type()
type_info * type () const ;
Returns the
corresponding to the exception held by this
.
4.2.2. get_raw_ptr()
void * get_raw_ptr () const ;
Returns the address of the exception held by this
. It is a pointer to the type
described by type(), so the caller will need to cast it to something compatible in order to use
this.
5. Use Cases
5.1. Lippincott Functions
Here is a lightly-modified example from our code base that shows a 100x speedup on Linux:
Now | With this proposal |
---|---|
|
|
|
|
Windows shows only a 2x speedup (1488ns -> 744ns) due to the DecodePointer() issue mentioned
above (160ns), as well as
being even slower (575ns). Essentially all of
the runtime is in those two functions. (Windows tests were conducted on different hardware, so the
results cannot be directly compared to Linux.)
5.2. Terminate Handlers
It is common practice to use terminate handlers to provide useful debugging information about the
failure. libstdc++ has a default handler that prints the type of the thrown exception using it’s
privileged access to the EH internals. Unfortunately, there is no way to do that in the general case
in a user-defined terminate handler. type() makes that information available from
in a portable way.
As a historical sidenote, that message from the terminate handler is what initially got me looking into the ABI for exception handling to figure out how that was done. If whoever wrote that code is reading this, thanks!
5.3. std :: expected
and Similar Types
These types become more useful with the ability to interact with the exception directly without rethrowing.
5.4. Error Handling in Futures
Error handling in Future chains naturally involves passing error objects into callbacks as
arguments. There has been an active discussion around avoiding
due to these issues. In addition to the direct performance benefits
of avoiding the unwinder, this also makes it easier to provide nicer APIs like
that only invokes the user’s callback when the types
match. This has a secondary benefit of avoiding a trip through the executor’s scheduler if the
callback isn’t to be called. It also has the (unconfirmed) potential of being implementable on GPUs
which don’t currently support exceptions.
6. It sounds like you just want faster exception handling. Why isn’t this just a QoI issue?
It has been 30 years since C++98 was finalized. Compilers seem to actively avoid optimizing for
speed in codepaths that involve throwing, which is usually a good choice. But this means that even
in trivial cases, they aren’t able to work their usual magic. Here is an example function that
should be reduced to a constant value of
, but instead goes through the full
/
process on all 3 major compilers. On my Linux desktop that means it takes 5600 cycles, when it
should take none.
int shouldBeTrivial () { try { throw 0 ; } catch ( int ex ) { return ex ; } return 1 ; };
Given how universal the poor handling of exceptions is, I don’t see much hope for improvement to the
extent proposed here in the realistic, non-trivial cases. Additionally, I think the ergonomics are
better if the exception object is made directly available than requiring the
/
blocks.
Additionally, as mentioned in §3.1 EWG Rapperswil 2018, EWG strongly favors adding this functionality to
, even if the performance was the same.
7. Related Future Work
These are related ideas that are not part of this proposal. If this proposal is well received, I’d like a straw poll of these to gauge the interest for future proposals.
7.1. Support dynamic dynamic_cast
using type_info
My initial implementation plan called for adding casting facilities to
and building the try_catch() logic on top of that. Since it ended up being the wrong route for the MSVC ABI, I
abandoned that plan, but it still provides useful independent functionality. Something like adding
the following methods on
:
template < typename T > bool convertable_to () template < typename T > T * dynamic_dynamic_cast ( void * ptr );
7.2. dynamic_any_cast
Currently
only supports exact-match casting. Using a similarly enhanced
it should
be able to support more flexible extractions.
7.3. Less copies in std :: make_exception_ptr ( E e )
I noticed that the current definition takes the exception by value and is defined as copying it. Should it take the exception object by forwarding reference and forward it? If consensus is yes, I’d be happy to either submit a new paper or just add that to the proposed wording here.