On Wed, 29 Mar 2023 at 10:07, Arthur O'Dwyer via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
On Tue, Mar 28, 2023 at 9:42 AM Phil Endecott via Std-Proposals <std-proposals@lists.isocpp.org> 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

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.
   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 "
Particularly, this section looks like it's (vaguely, handwavily) talking about the situation with std::any:

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?

Std-Proposals mailing list