Document Number: | p2927r0 |
---|---|
Date: | 2023-06-16 |
Target: | EWG, LEWG, LEWGI |
Reply to: | gorn@microsoft.com |
Provide facility to observe exceptions stored in std::exception_ptr
without throwing or catching exceptions.
This is a followup to two previous papers in this area:
Date | Link | Title |
---|---|---|
Feb 7, 2018 | https://wg21.link/p0933 | Runtime introspection of exception_ptr |
Oct 6, 2018 | https://wg21.link/p1066 | How to catch an exception_ptr without even try-ing |
These papers received positive feedback. In 2018 Rapperswil meeting, EWG expressed strong desire in having such facility:
Does EWG want a non throwing mechanism to get to the exception held by exception_ptr even if the performance was the same
SF | F | N | A | SA |
---|---|---|---|---|
16 | 8 | 1 | 0 | 0 |
LEWG looked at this at SanDiego 2018 and encouraged to come back after addressing the following points:
This paper brings back exception_ptr inspection facility in a simplified form addressing the earlier feedback.
We introduce a single function try_cast
that takes std::exception_ptr
as an argument e
and returns a pointer to an object referred to by e
.
template <typename T>
const std::remove_cvref_t<T>*
try_cast(const std::exception_ptr& e) noexcept;
If exception_ptr
is not empty and std::remove_cvref_t<T>
is the type of the stored exception E
or its unambiguous base, a const pointer to the stored exception is returned; otherwise nullptr
is returned.
Example:
Given the following error classes:
struct Foo {
virtual ~Foo() {}
int i = 1;
};
struct Bar : Foo, std::logic_error {
Bar() : std::logic_error("This is Bar exception") {}
int j = 2;
};
struct Baz : Bar {};
The execution of the following program
int main() {
const auto exp = std::make_exception_ptr(Baz());
if (auto* x = try_cast<Baz>(exp))
printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
if (auto* x = try_cast<Bar>(exp))
printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
if (auto* x = try_cast<Foo>(exp))
printf("got '%s' i: %d\n", typeid(*x).name(), x->i);
}
results in this output:
got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' i: 1
See implementation for GCC and MSVC using available (but undocumented) APIs https://godbolt.org/z/ErePMf66h.
Let's look at existing facilities in the library that serve somewhat similar purpose:
any_cast:
template<class T> T any_cast(const any& operand);
template<class T> T any_cast(any& operand);
template<class T> T any_cast(any&& operand);
template<class T> const T* any_cast(const any* operand) noexcept;
template<class T> T* any_cast(any* operand) noexcept;
Last two overloads perform inspection of a stored value and give a pointer to it
if the typeid matches, otherwise return nullptr
.
The first three, throw bad_any_cast
exception if the stored value does not match the requested type.
get/get_if:
// get forms that takes a reference and throw if the type is not active alternative
template<class T, class... Types> T* get_if(variant<Types...>* pv) noexcept;
template<class T, class... Types> const T* get_if(const variant<Types...>* pv) noexcept;
Similarly to any_cast
, get and get_if offer throwing versions that take a reference
to a variant and a non-throwing that take a variant by pointer and return a pointer
to the value of the requested type or nullptr
if it is not an active alternative.
exception_ptr:
Q: Should we follow the the examples above and have two flavors of exception_ptr inspection,
one that throws if the requested type E
is not stored and one that does not.
A: No. The goal of this facility is to provide a way to inspect a stored exception value without relying on throwing exception.
Q: Non-throwing versions of dynamic_cast
, any_cast
and get_if
all take a pointer to a value to be inspected,
should this API take the pointer as well?
A: dynamic_cast
and any_cast
relies on a distinction on how the value is passed
(by pointer or by reference) to select what semantic to provide. In our case,
we always perform a conditional access, thus, requiring a pointer would be a needless syntactic noise.
Q: The variant
and any
inspection APIs offers a version that allows both const and
non const access to the stored value. Should we do the same for exception_ptr
?
A: No. N4928/[propagation]/7 states that:
For purposes of determining the presence of a data race, operations on exception_ptr objects shall access and modify only the exception_ptr objects themselves and not the exceptions they refer to.
Offering the API flavor that allows mutation of the stored exception has potential of injecting a data race. Cpp core guidelines recommend catching exception by const reference.
In this paper we only offer a try_cast
API that offer an ability to inspect,
but not modify stored exception. If in the future, we will discover an important use
case that requires changing the stored exception a different API with lengthier
and less convenient name, say try_cast_mutable
or something along those lines can be added.
The desire is to offer the simplest name for the most common use case that is a good default for the majority of the users.
Similarly, we chose to specify the desired type in the simplest possible form, i.e:
auto* x = try_cast<const int*>(eptr); // no
auto* x = try_cast<int*>(eptr); // no
auto* x = try_cast<int>(eptr); // yes
Thus, the thought process above led to the following API shape:
template <typename T>
const std::remove_cvref_t<T>*
try_cast(const std::exception_ptr& e) noexcept;
Q: Could we reuse any_cast
name?
A: No. It encodes any
in its name strongly hinting it is meant
to be used with std::any.
Q: Maybe get_if
would work?
A: Possibly, but, it has slightly different semantics.
Unlike any_cast
, dynamic_cast
and offered here try_cast
have
cast
in its name, implying that it does not have to be exact match,
whereas get_if
expects the type to be exact match from the list of variant
alternatives.
It is possible that in the future, we will have a unifying facility (pattern matching, for example) that would allow uniform access across variant, any, exception_ptr and other types. Finding such facility is out of scope of this paper.
Q: Why const std::remove_cvref_t<T>*
in the return value? Could it be just T?
A: The intent here is to make sure that people who habitually write catch (const E& e) { ... }
can continue doing it with try_cast<const E&>
, as opposed to getting a compilation error. This is an error from the category:
The compiler/library knows what you mean, but, will force you to write exactly as it wants.
cast_exception_ptr<E>
cast_exception<E>
try_cast_exception_ptr<E>
try_cast_exception<E>
catch_as<E>
We expect that try_cast
will be integrated in the pattern matching facility and
will allow inspection of exception_ptr
as follows:
inspect (eptr) {
<logic_error> e => { ... }
<exception> e => { ... }
nullptr => { puts("no exception"); }
__ => { puts(some other exception"); }
}
GCC, MSVC implementation is possible using available (but undocumented) APIs https://godbolt.org/z/ErePMf66h. Implementability was also confirmed by MSVC and libstdc++ implementors.
A similar facility is available in Folly and supports Windows, libstdc++, and libc++ on linux/apple/freebsd.
https://github.com/facebook/folly/blob/v2023.06.26.00/folly/lang/Exception.h https://github.com/facebook/folly/blob/v2023.06.26.00/folly/lang/Exception.cpp
Implementation there under the names: folly::exception_ptr_get_object folly::exception_ptr_get_type
Extra constraint imposed by MSVC ABI: it doesn't have information stored to do a full dynamic_cast. It can only recover types for which a catch block could potentially match.
This does not conflict with the try_cast
facility offered in this paper.
A version of exception_ptr inspection facilities is deployed widely in Meta as a part of Folly's future continuation matching.
TBD
Many thanks to those who provided valuable feedback, among them: Aaryaman Sagar, Arthur O'Dwyer, Jan Wilmans, Joshua Berne, Lee Howes, Michael Park, Peter Dimov, Ville Voutilainen, Yedidya Feldblum.
https://godbolt.org/z/ErePMf66h (gcc and msvc implementation)
https://wg21.link/p0933 Runtime introspection of exception_ptr
https://wg21.link/p1066 How to catch an exception_ptr without even try-ing
https://wg21.link/p1371 Pattern Matching