C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Wed, 29 Mar 2023 10:56:34 -0500
On Wed, 29 Mar 2023 at 10:07, Arthur O'Dwyer via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> 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.
>

Not if std::type_info already supports that functionality, which it
presumptively does to support exception matching. Or if it doesn't, it
might be possible to add it without ABI breaks, since std::type_info
objects are usually created by the implementation and not by users
directly. (It would probably be acceptable for the functionality to fail
when a std::type_info from an old object file is used, as long as it does
so safely.)

So the question is whether it would be possible in all implementations to
add any_base_cast / any_dynamic_cast without ABI break. We know that it is
in libstdc++ and libc++. Do we have an answer for MSVC-STL?

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 think it would be better to be able to do this without mentioning
exceptions. The original motivation just talks about `std::any`, and that's
much like `std::pair<const std::typeinfo*, void*>` but with ownership -
sort of `std::pair<const std::typeinfo*, std::$smart_ptr<void>>`. It would
be very odd to say "oh, if you need to cast a std::any to a base type you
should use std::exception_ptr instead".

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
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2023-03-29 15:56:48