C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Get base class from std::any

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Wed, 29 Mar 2023 11:07:41 -0400
On Tue, Mar 28, 2023 at 9:42 AM Phil Endecott via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Thiago Macieira wrote:
> > On Monday, 27 March 2023 10:30:22 PDT Phil Endecott via Std-Proposals
> wrote:
> >> Clearly the throw-catch implementation has a huge overhead. Making it
> >> possible to do this portably, i.e. by standardising the functionality
> >> provided by std::type_info::__do_catch, would be useful IMO.
> >
> > It's unnecessary to standardise __do_catch. std::any is a Standard
> Library
> > type; so long as it is possible in all implementations and reasonably
> fast in
> > most, it can be standardised. You've shown it's possible in all that are
> > compliant (disabling RTTI or exceptions isn't compliant), so one hurdle
> is
> > overcome. You've shown it is fast in one implementation, so half of the
> other
> > is done too.
>
> Right. There are various choices:
>
> 1. Add something like any_base_cast to std::any, and hope that library
> implementers will use private features of the ABI to provide a fast
> implementation.
>

This would be an ABI break: the vendor would have to add a new virtual
function to the vtable of the internal type used by `std::any`, which means
you'd get [ODR violations and] segfaults when you pass an "old-style"
std::any object across an ABI boundary to a function expecting the
"new-style" std::any.

2. Add a runtime_cast (*) to the core language, which users can use to
> implement their own Any type with a base cast, and no doubt other uses that
> I've not thought of.
>

(*) Sketch of runtime_cast:
> void* runtime_cast(const typeinfo* t, const typeinfo* u, void* p);
> Precondition: p is nullptr or points to an object of type u.
> Postcondition:
> if p is nullptr, the return value is nullptr.
> if t == u, the return value is p.
> if t is a base class of u, the return value is a pointer to the
> base object of p of type t.
> else the return value is nullptr.
>

Contra Thiago, this is *not* just `dynamic_cast`: `dynamic_cast` is
parameterized on two actual static types `T` and `U`, and requires that you
know both of those types statically at the call-site. Instead, what we have
here is a call-site that doesn't statically know both `T` and `U`: instead,
there's one place in the code that knows `U` (and can type-erase it into
`typeid(U)`), and another place in the code that knows `T` and wants to
cast `p` to `T`.
Notice that the second place in the code — the place that knows the
destination type `T` — *doesn't need to type-erase it!* At least not for
Phil E's use-case. For Phil's use-case, all we need is:

    template<class B>
      B *runtime_cast(const std::typeinfo& ti, void* p);
    Precondition: Let D be the type such that ti == typeid(D). p points to
an object of type D, or p is null.
    Returns: If p is non-null, dynamic_cast<B>((D*)p). Otherwise, null.

However, another way to achieve the same goal would be to piggyback on
`exception_ptr`. See Mathias Stearn's "
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1066r1.html
Particularly, this section looks like it's (vaguely, handwavily) talking
about the situation with std::any:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1066r1.html#dynamic_any_cast

Using standard C++, your call-sites would be
    template<class D> void AnyImpl<D>::getbaseptr() override { throw &d_; }
    template<class B> B *any::as_base() { try { impl_->getbaseptr(); }
catch (B *b) { return b; } catch (...) { return nullptr; } }

Using your `runtime_cast` as sketched above, your call-sites would be
    template<class D> std::pair<const std::typeinfo*, void*>
AnyImpl<D>::getbaseptr() override { return { &typeid(D), &d_ }; }
    template<class B> B *any::as_base() { auto [ti, p] =
impl_->getbaseptr(); return std::runtime_cast(typeid(B), ti, p); }

Using my `runtime_cast` as sketched above, your call-sites would be
    template<class D> std::pair<const std::typeinfo*, void*>
AnyImpl<D>::getbaseptr() override { return { &typeid(D), &d_ }; }
    template<class B> B *any::as_base() { auto [ti, p] =
impl_->getbaseptr(); return std::runtime_cast<B>(ti, p); }

Using P1066 facilities, your call-sites would be
    template<class D> std::exception_ptr AnyImpl<D>::getbaseptr() override
{ return std::make_exception_ptr(&d_); }
    template<class B> B *any::as_base() { auto ex = impl_->getbaseptr();
return ex.handle([](B *b) { return b; }).value_or(nullptr); }

For the record, I'm not a fan of P1066's complicated metaprogramming in
`handle` nor the way it bakes `std::optional` into the API of
`std::exception_ptr`. I don't think the P1066R1 API is perfect; I think
it needs more massaging. But it is, IMHO, a step in the right direction —
`std::exception_ptr` is obviously a better choice to smuggle this
information across the ABI boundary than `std::pair<const std::typeinfo*,
void*>`. I encourage you to think in that general direction.

I'm also curious whether Phil E has any other use-cases in mind. Is there a
good reason to provide the super-generalized `void *runtime_cast(ti, ti,
p)` instead of `B *runtime_cast<B>(ti, p)`? What's a situation where you'd
want that extra generality?

–Arthur

Received on 2023-03-29 15:07:54