| Document Number: | p2927r1 | 
|---|---|
| Date: | 2024-02-14 | 
| Target: | LEWG | 
| Revises: | p2927r0 | 
| Reply to: | Arthur O'Dwyer (arthur.j.odwyer@gmail.com), Gor Nishanov (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. This was reaffirmed in 2023 Kona meeting.
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 T* 
try_cast(const exception_ptr& e) noexcept;
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 = std::try_cast<Baz>(exp))
        printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j); 
    if (auto* x = std::try_cast<Bar>(exp))
        printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
    if (auto* x = std::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/E8n69xKjs.
Previous revision tentatively proposed a complicated signature imitating the syntax of a catch-parameter, as in old::try_cast<const std::exception&>(p).
In Kona, we were convinced to simplify the signature to assume catch-by-const-reference no matter what: std::try_cast<std::exception>(p).
old::try_cast<const std::exception&>(p)
and old::try_cast<std::exception>(p). The new syntax removes that needless variability of style.old::try_cast<const std::exception&>(p) in order to catch by reference;
that's unnecessarily verbose and hard to read. The new syntax is short and readable.old::try_cast<std::exception&>(p) to catch by non-const reference; this is sometimes legitimate but usually it's just an unnecessary violation of const-correctness. Users might omit the const out of inattention or laziness. The new syntax always returns const*, privileging the common and const-correct case.const_cast<std::exception*>(std::try_cast<std::exception>(p)). The const_cast is visible and greppable; this is a good thing.old::try_cast<int(&)()>(p). Such a catch handler is physically impossible to hit, because you can't throw a function; functions are not objects. The new syntax admits only old::try_cast<int()>(p), which is ill-formed by our new Mandates element because int() is not an object type. Similarly, we disallow old::try_cast<int[5]>(p) via the Mandates element.P2927R0 proposed that try_cast should be able to catch pointer types, just like an ordinary catch clause. That is, not only were you allowed to inspect a thrown Derived object with old::try_cast<const Base&> (which would return a possibly null const Base*), you were also allowed to inspect a thrown Derived* object with old::try_cast<const Base*> (which would return a possibly null const Base**). This turned out to be unimplementable. When a catch-handler catches Derived* as Base*, it may need to adjust the pointer for multiple and/or virtual base classes. The pointer caught by the core language, then, is a temporary. We can't return a const Base** pointing to that temporary adjusted pointer, because there's nowhere for the temporary adjusted pointer to live after the call to try_cast has returned.
In other words, the new design has a strict invariant: the pointer returned from try_cast always points to the in-flight exception object itself. It never points to any other object, such as a temporary or global. Thus, we must disallow:
    using IntPtr = int*;
    std::nullptr_t np;
    auto p = std::make_exception_ptr(np);
      // The in-flight exception object is of type std::nullptr_t
    const IntPtr *ex = old::try_cast<IntPtr>(p);
      // ex cannot possibly point to the in-flight exception object, because the in-flight object is not an IntPtr!
    try {
        std::rethrow_exception(p);
    } catch (const IntPtr& ex) {
        // OK, ex refers to a temporary that lives only as long as this catch block
    }
Our solution is simply to extend our Mandates element to also forbid std::try_cast with a template argument of pointer or pointer-to-member type; these are the only two kinds of types where a core-language catch-handler parameter would sometimes bind to a temporary, so these are the only kinds of types we need to forbid. Later, we found that [ScyllaDB] had independently implemented the same solution (i.e. explicitly forbid pointer types) in 2022.
Throwing pointers is rare — probably unheard of in real code. This does prevent users from using std::try_cast<const char*>(p) to inspect the results of throw "foo", which comes up sometimes in example code; but it shouldn't happen in real code.
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"); }
}
cast_exception_ptr<E>cast_exception<E>try_cast_exception_ptr<E>try_cast_exception<E>catch_as<E>exception_cast<E>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.
Arthur has implemented P2927R1 std::try_cast in his fork of libc++; see [libc++] and [Godbolt].
A version of exception_ptr inspection facilities is deployed widely in Meta as a part of Folly's future continuation matching.
ScyllaDB implements almost exactly the wording of this proposal, under the name try_catch<E>(p); see [ScyllaDB]. The only difference is that they return E* instead of const E*. We hear from them that they don't actually use the mutability for anything; and even if they did, they could add const_cast as mentioned above.
In section [exception.syn] add definition for try_cast
as follows:
exception_ptr current_exception() noexcept;
[[noreturn]] void rethrow_exception(exception_ptr p);
template <class E>
  const E* try_cast(const exception_ptr& p) noexcept;
template <class T> [[noreturn]] void throw_with_nested(T&& t);
exception_ptr objects shall access and modify only the exception_ptr objects themselves and not the exceptions they refer to. Use of rethrow_exception or try_cast
on exception_ptr objects that refer to the same exception object shall not introduce a data race.
E is a cv-unqualified complete object type. E is not an array type. E is not a pointer or pointer-to-member type. [Note: When E is a pointer or pointer-to-member type, a handler of type const E& can match without binding to the exception object itself. —end note]
p,
if p is not null and a handler of type const E& would be a
match [except.handle] for that exception object.
Otherwise, nullptr.
Many thanks to those who provided valuable feedback, among them: Aaryaman Sagar, Barry Revzin, Gabriel Dos Reis, Jan Wilmans, Joshua Berne, Lee Howes, Lewis Baker, Michael Park, Peter Dimov, Ville Voutilainen, Yedidya Feldblum.
https://godbolt.org/z/E8n69xKjs (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
https://github.com/scylladb/scylladb/blob/946d281/utils/exceptions.hh#L128-L151 ScyllaDb
An implementation of try cast and libc++ and matching godbolt:
https://github.com/Quuxplusone/llvm-project/commit/6e20a0b9d5a2280bfab8ab42bee841cfbcc4a8bd