C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Base class reflection

From: Billy Martin <bmartin_at_[hidden]>
Date: Fri, 3 Feb 2023 16:03:58 -0800
Interesting.

One of the other things I had explored while I was working on this problem
was, "can you dynamic_cast from a void*?" It turns out you cannot,
according to the standard. This is a bit curious, since the other
conversions that you are allowed to do to void* all require some degree of
assumptions about what exactly that void* is pointing to in order to use
the resulting pointer. However, I suppose in the case of dynamic_cast, the
program would blow up *with the cast itself* (if for example the rtti is
missing because the void* points to a non-polymorphic type), whereas the
other casts will simply produce an invalid pointer in the worst case.

Of course, Edward's code doesn't have this problem, because he's supplying
the rtti explicitly via typeid. In other words, it should work on
non-polymorphic types, too. Just like the throw-catch trick.

I wonder if there is some way to provide a cast from void* that would
essentially tell the compiler to write the equivalent of Edward's code.
It's, like, not quite a static_cast, but it's not quite a dynamic_cast
either. It requires a bit of extra information because you have to pass the
typeid of the base as an argument to the cast.

Maybe there could be some sort of cast where you pass it a void* and a
typeid, and it essentially says, "do a static_cast/dynamic_cast, but assume
that the void* points to an object that is of the type represented by
typeid". Do you guys think something like that could be implemented?

It wouldn't get rid of the need for std::bases because it wouldn't be
constexpr, but it might provide a simpler solution for people who don't
care about constexpr and are happy doing their type erasure via void* and
passing typeids around.

Actually, maybe the standard could provide an object that, internally, is
essentially a typeid and a void*, but you create it from a T*, so in some
sense we can have a type-safe way of knowing that the void* does indeed
point to the right type of object, and *maybe* could even be constexpr
compatible though compiler magic.

For example, something like:

class std::any_pointer
{
 const VOID_PTR ptr; // VOID_PTR is an implementation defined type that is
like void* but constexpr compatible
 typeid const ti;
public:
 template<typename T> constexpr any_pointer(T* pt) : ptr(pt), ti(typeid(T))
{}
 template<typename T> constexpr T* static_cast_to() const { /* do Edward's
code, return nullptr if the cast would be invalid */}
 template<typename T> constexpr T* dynamic_cast_to() const;
//... etc ...
}

Note that a non-constexpr version of the above class could be implemented
using the throw/catch trick. The benefit of making it part of the standard
library is making the compiler do the dirty work for us:

a) this might make it possible to be constexpr, and

b) means that people like me can do this sort of cast without having to use
"expert level" dirty tricks.

Note that even with this, we can't fully solve the clone() problem in the
general case. It would solve the simple example I gave and nearly all
practical applications though. You would still need std::tr2::direct_bases
to work out the case where you have multiple inheritance with multiple
non-virtual bases of the same type.

Billy

On Fri, Feb 3, 2023 at 1:14 AM Edward Catmur <ecatmur_at_[hidden]> wrote:

>
>
> On Wed, 1 Feb 2023 at 19:47, Arthur O'Dwyer via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> That code is vastly more complicated than you need.
>>
>> First observation: Remove all the `constexpr`. Some of it isn't even
>> valid C++, according to Clang (namely where you make constexpr virtual
>> methods of a class with virtual bases). Luckily none of it is needed.
>>
>> Second observation: You use "concept overloading" in an overload set for
>> `SafelyAssignPointer`, but you don't need to. You can replace both
>> overloads with the following simple code:
>>
>>
>> template<class Base, class T>
>>
>> void SafelyAssignPointer(Base*& bp, T* dp) {
>>
>> if constexpr (std::is_base_of_v<Base, T>) {
>>
>> bp = dp;
>>
>> }
>>
>> }
>>
>> In fact, you can *almost* replace that with simply this:
>>
>> template<class T, class U>
>>
>> void SafelyAssign(T& t, U u) {
>>
>> if constexpr (requires { t = u; }) {
>>
>> t = u;
>>
>> }
>>
>> }
>>
>> except that you've incorrectly added explicit template arguments to the
>> call-site:
>>
>> SafelyAssignPointer<std::remove_const_t<Base>,
>> Derived>(te_pointer_to_base->typed_pointer, pointer_to_derived)
>> needs to be changed to the more appropriate syntax for calling a function
>> template with deducible type parameters:
>>
>> SafelyAssign(te_pointer_to_base->typed_pointer, pointer_to_derived)
>>
>> So at this point we have:
>> https://godbolt.org/z/zG7911xE1
>> But we haven't gotten to the part that "needs" std::bases yet. Let's keep
>> going.
>> We do a little more refactoring (applying the same `if constexpr` idea at
>> least once more) and end up with this:
>> https://godbolt.org/z/GPe3jd7b5
>> Now we're beginning to get to the part that "needs" a list of base
>> classes.
>>
>> Let's look at TryToSafelyAssignTypelessPointer again. That's a free
>> function where the first argument is a pointer to a TypelessPointer (which
>> is an OOP type). That suggests that it should be a member function of the
>> OOP type. Let's make it a member function.
>>
>> struct TypelessPointer {
>>
>> template<class D>
>>
>> void TryToSafelyAssign(D *pd);
>>
>> virtual bool IsNull() const = 0;
>>
>> virtual ~TypelessPointer() = default;
>>
>> };
>>
>>
>> template<class T>
>>
>> struct TypeErasedPointer : TypelessPointer {
>>
>> T *p_ = nullptr;
>>
>> bool IsNull() const override { return p_ == nullptr; }
>>
>> };
>>
>>
>> template<class D>
>>
>> void TypelessPointer::TryToSafelyAssign(D *pd) {
>>
>> // In here, we want to know if `pd` can be converted to
>> the-type-held-by-my-`p_`,
>>
>> // but we haven't got access to my `p_` because that's in the base
>> class.
>>
>> !!!
>>
>> }
>>
>> And boom goes the dynamite. You're correct, this situation is tricky to
>> handle. We have just one thing going for us: we've already committed to
>> using dynamic_cast, so we can use dirty RTTI tricks here if they get the
>> job done. For example:
>>
>> struct TypelessPointer {
>>
>> template<class D>
>>
>> void TryToSafelyAssign(D *pd) {
>>
>> AssignHelper([&]() { throw pd; });
>>
>> }
>>
>> virtual void AssignHelper(std::function<void()>) = 0;
>>
>> virtual bool IsNull() const = 0;
>>
>> virtual ~TypelessPointer() = default;
>>
>> };
>>
>>
>> template<class T>
>>
>> struct TypeErasedPointer : TypelessPointer {
>>
>> T *p_ = nullptr;
>>
>> bool IsNull() const override { return p_ == nullptr; }
>>
>> void AssignHelper(std::function<void()> f) override {
>>
>> try {
>>
>> f();
>>
>> } catch (T *p) {
>>
>> p_ = p;
>>
>> } catch (...) {}
>>
>> }
>>
>> };
>>
>
> For those who are (like me) interested in how this throw-catch trick
> works, part of the answer is that the rtti for the derived class type
> contains a list of its base classes. On Itanium this is accomplished by
> having the type_info for the derived class an instance of
> __cxxabiv1::__vmi_class_type_info (a derived class of std::type_info) which
> holds a list of bases via its __base_info member. (The "vmi" stands for
> "virtual [and/or] multiple inheritance"; a class with a single public
> non-virtual base uses the simpler __cxxabiv1::__si_class_type_info class
> for its type_info.)
>
> The actual implementation of the attempted cast-to-base is in __do_upcast:
> https://github.com/gcc-mirror/gcc/blob/5c43f06c228d169c370e99fa009154344fa305b8/libstdc%2B%2B-v3/libsupc%2B%2B/vmi_class_type_info.cc#L304
>
> We can make use of this writing platform-dependent code without the cost
> of actually invoking the exception handling machinery:
> https://godbolt.org/z/91PTh6ePT
>
> void AssignHelper(std::type_info const& ti, void* p) override {
> if (ti.__do_upcast(&reinterpret_cast<__cxxabiv1::__class_type_info
> const&>(typeid(T)), &p))
> p_ = static_cast<T*>(p);
> else
> throw std::runtime_error("couldn't find base class");
> }
>
> Note that this is fully type-erased; the compiler serializes the
> inheritance information into the type_info classes such that it can be
> accessed across TUs. So certainly if we were to expose the base class
> reflection information at compile time we could accomplish the same. Also
> note that it is, for much the same reason, non-constexpr; the only
> constexpr operation on std::type_info instances is equality comparison. So
> exposing the base information via reflection would be an improvement.
>
> Of course we're writing our own type-erasure, so using std::function here
>> feels a bit like cheating. Let's use a function pointer and void* cookie
>> instead. Now we have this:
>> https://godbolt.org/z/8Kehcxecq
>>
>> Notice that our *big breakthrough* came when we said, "Hey, this free
>> function looks a lot like it ought to be a member function." That is, the
>> fundamental problem here wasn't anything at the low level but rather a
>> problem of defining our *API boundaries*. As soon as we said,
>> "TypelessPointer ought to be able to TryToSafelyAssign a new value *to
>> itself*," and thinking about the information accessible to *TypelessPointer
>> specifically*, rather than thinking of that operation as something
>> outside of our API, everything fell into place pretty much naturally.
>> We still had to rely on RTTI in the form of try/catch, and yes that is a
>> dirty expert-trivia trick... but as soon as we knew we were trying to do a
>> dynamic upcast from "unknown" to "known," and looked up the dirty trick for
>> that, everything else fell into place naturally.
>>
>> That seems to be the main difficult part in this code sample, right? I'm
>> not motivated to go further down than that if I don't have to. :)
>> Here's the "final" version of the code again:
>> https://godbolt.org/z/8Kehcxecq
>>
>> Cheers,
>> Arthur
>>
>>
>> On Tue, Jan 31, 2023 at 9:53 PM Billy Martin <bmartin_at_[hidden]> wrote:
>>
>>>
>>>
>>> /* This will demonstrate a simple reference-counting smart pointer with
>>> clone() functionality,
>>> that could be implemented using experimental std::bases
>>>
>>> Permission hereby granted to use this code for any purpose that will
>>> directly or indirectly
>>> lead to std::bases actually being a part of the C++ standard.
>>>
>>> Billy
>>> */
>>>
>>> #include <type_traits>
>>> #include <stdexcept>
>>> #include <iostream>
>>>
>>> /****** Some polymorphic classes for testing
>>> ******/
>>>
>>> namespace TestClasses {
>>> struct A
>>> {
>>> int a = 0;
>>> virtual constexpr int get() const { return a; }
>>> virtual ~A() = default;
>>> };
>>>
>>> struct B : virtual A
>>> {
>>> int b = 1;
>>> constexpr int get() const override { return b; }
>>> };
>>>
>>> struct C : virtual A
>>> {
>>> int c = 2;
>>> constexpr int get() const override { return c; }
>>> };
>>>
>>> struct D : B, C
>>> {
>>> int d = 3;
>>>
>>> constexpr int get() const override { return d; }
>>> };
>>>
>>> struct E
>>> {
>>> int e = 4;
>>> };
>>> }
>>>
>>>
>>> /****** Simple Type Erasure for pointers
>>> ******/
>>>
>>> struct TypelessPointer
>>> {
>>> virtual constexpr bool IsNull() const = 0;
>>> virtual ~TypelessPointer() = default;
>>> };
>>>
>>> template<typename T>
>>> struct TypeErasedPointer : TypelessPointer
>>> {
>>> T* typed_pointer = nullptr;
>>>
>>> constexpr bool IsNull() const override { return typed_pointer ==
>>> nullptr; }
>>> };
>>>
>>>
>>> /***** Some concepts we need
>>> ******/
>>>
>>> template<class D, class B>
>>> concept DerivedFrom = std::is_base_of<B, D>::value;
>>>
>>> template<class D, class B>
>>> concept NotDerivedFrom = !std::is_base_of<B, D>::value;
>>>
>>> template<typename T>
>>> concept Copyable = std::is_copy_constructible<T>::value;
>>>
>>> template<typename To, typename From>
>>> concept PointerConvertible = std::is_convertible<From*, To*>::value;
>>>
>>>
>>> /***** Building up the Pointer assignment functionality we need
>>> *****/
>>>
>>> template<typename Base, typename Derived> requires DerivedFrom<Derived,
>>> Base>
>>> constexpr void SafelyAssignPointer(Base*& bp, Derived* dp)
>>> {
>>> // I can safely do this conversion because I know both types,
>>> Derrived and Base, at compile time
>>> bp = static_cast<Base*>(dp);
>>> }
>>>
>>> template<typename NotBase, typename NotDerived> requires
>>> NotDerivedFrom<NotDerived, NotBase>
>>> constexpr void SafelyAssignPointer(NotBase*& bp, NotDerived* dp)
>>> {
>>> // this overload represents an invalid assignment, so don't do
>>> anything
>>> // (wouldn't need this with proper working version of std::bases)
>>> }
>>>
>>> template<typename Base, typename Derived>
>>> constexpr bool TryToSafelyAssignTypelessPointer(TypelessPointer*
>>> typeless_pointer_to_base, Derived* pointer_to_derived)
>>> {
>>> // I am not sure if Base is the type you are looking for but I can
>>> perform a runtime check and assign it if it is.
>>> auto te_pointer_to_base = dynamic_cast
>>> <TypeErasedPointer<std::remove_const_t<Base>>*>(typeless_pointer_to_base);
>>> if (te_pointer_to_base != nullptr)
>>> SafelyAssignPointer<std::remove_const_t<Base>,
>>> Derived>(te_pointer_to_base->typed_pointer, pointer_to_derived);
>>> return te_pointer_to_base != nullptr;
>>> }
>>>
>>> template<typename ...A>
>>> constexpr void Please(A... do_things) {}
>>>
>>> template<typename Derived, typename ...Bases>
>>> constexpr void SetTypelessPointerToBase(TypelessPointer*
>>> typeless_pointer_to_base, Derived* pointer_to_derived)
>>> {
>>> // If you give me a list of base classes then I can check each one
>>> at runtime to try and find the one you gave me.
>>> if (typeless_pointer_to_base == nullptr) return;
>>> Please(TryToSafelyAssignTypelessPointer<Bases,
>>> Derived>(typeless_pointer_to_base, pointer_to_derived)...);
>>> // I should check the Derived class itself, also
>>> TryToSafelyAssignTypelessPointer<Derived,
>>> Derived>(typeless_pointer_to_base, pointer_to_derived);
>>>
>>> if (pointer_to_derived != nullptr &&
>>> typeless_pointer_to_base->IsNull()) {
>>> throw std::runtime_error("couldn't find base class");
>>> }
>>> }
>>>
>>> template<typename Derived, typename BaseList>
>>> struct BaseListUnpacker {};
>>>
>>> template<typename Derived, template <typename ...> typename TypeList,
>>> typename ...Bases>
>>> struct BaseListUnpacker<Derived, TypeList<Bases...>>
>>> {
>>> // this specialization exists solely to unpack the list of bases
>>> and pass it along
>>> static constexpr void SetTyplessPointer(TypelessPointer*
>>> typeless_pointer_to_base, Derived* pointer_to_derived)
>>> {
>>> SetTypelessPointerToBase<Derived,
>>> Bases...>(typeless_pointer_to_base, pointer_to_derived);
>>> }
>>> };
>>>
>>>
>>> /***** Here we start implementing the smart pointer
>>> *****/
>>>
>>> struct TypelessObjectWithRefCounter
>>> {
>>> // Because the Smart Pointer wants to work the polymophic objects,
>>> // we're using a Type erased object to manage storage of the object.
>>> // We'll store the object with the ref count to save allocations
>>> long ref_count = 1;
>>>
>>> constexpr void AddRef() { ++ref_count; }
>>> constexpr void DecRef() { --ref_count; }
>>> constexpr bool IsExpired() const { return ref_count == 0; }
>>>
>>> virtual constexpr TypelessObjectWithRefCounter* clone() const = 0; //
>>> note that Type Erasure things can clone themselves easily in C++
>>> virtual constexpr void SetTypelessToPointToObject(TypelessPointer*
>>> teptr) = 0; // this can't be implmented without std::bases
>>>
>>> constexpr TypelessObjectWithRefCounter() = default;
>>> virtual constexpr ~TypelessObjectWithRefCounter() = default;
>>> constexpr TypelessObjectWithRefCounter(const
>>> TypelessObjectWithRefCounter&) = delete;
>>> constexpr
>>> TypelessObjectWithRefCounter(TypelessObjectWithRefCounter&&) = delete;
>>> constexpr TypelessObjectWithRefCounter& operator=(const
>>> TypelessObjectWithRefCounter&) = delete;
>>> constexpr TypelessObjectWithRefCounter& operator=(TypelessObjectWithRefCounter&&)
>>> = delete;
>>> };
>>>
>>> template<typename T>
>>> struct RefCountingSmartPointer
>>> {
>>> TypelessObjectWithRefCounter* ptr_to_ref = nullptr; // contains
>>> storage for the actual object (might be different type than T)
>>> T* ptr_to_object = nullptr; // typed pointer to object
>>>
>>> constexpr T* get() const { return ptr_to_object; }
>>> constexpr T* operator*() const { return get(); }
>>> constexpr T* operator->() const { return get(); }
>>>
>>> constexpr void AddRef() const
>>> {
>>> if(ptr_to_ref != nullptr) ptr_to_ref->AddRef();
>>> }
>>>
>>> constexpr void DecRef() const
>>> {
>>> if (ptr_to_ref != nullptr) {
>>> ptr_to_ref->DecRef();
>>> if (ptr_to_ref->IsExpired()) {
>>> delete ptr_to_ref;
>>> }
>>> }
>>> }
>>>
>>> constexpr RefCountingSmartPointer() = default;
>>>
>>> constexpr RefCountingSmartPointer(TypelessObjectWithRefCounter*
>>> ptr_to_ref_, T* ptr_to_object_)
>>> : ptr_to_ref(ptr_to_ref_),
>>> ptr_to_object(ptr_to_object_)
>>> {}
>>>
>>> constexpr ~RefCountingSmartPointer()
>>> {
>>> DecRef();
>>> }
>>>
>>> // templated copy constructor, allows pointer conversion to work
>>> the same as with regular pointers
>>> template<typename F> requires PointerConvertible<T, F>
>>> constexpr RefCountingSmartPointer(const RefCountingSmartPointer<F>&
>>> copy)
>>> : ptr_to_ref(copy.ptr_to_ref),
>>> ptr_to_object(copy.ptr_to_object)
>>> {
>>> AddRef();
>>> }
>>>
>>> template<typename F> requires PointerConvertible<T, F>
>>> constexpr RefCountingSmartPointer& operator=(const
>>> RefCountingSmartPointer<F>& copy)
>>> {
>>> copy.AddRef();
>>> auto temp_ref = copy.ptr_to_ref;
>>> ptr_to_object = copy.ptr_to_object;
>>> DecRef();
>>> ptr_to_ref = temp_ref;
>>> }
>>>
>>> // move constructor/assignment omitted for brevity
>>>
>>> constexpr RefCountingSmartPointer<std::remove_const_t<T>> clone()
>>> const
>>> {
>>> // returns a smart pointer to a new copy of whatever this
>>> points to (if it has a copy constructor)
>>> if (ptr_to_ref == nullptr) return {};
>>> auto ref_copy = ptr_to_ref->clone();
>>> if (ref_copy == nullptr) throw std::logic_error("trying to
>>> clone an object that is not copyable");
>>> // tricky part is getting the ptr_to_object
>>> TypeErasedPointer<std::remove_const_t<T>> te_ptr_to_cloned_obj;
>>> ref_copy->SetTypelessToPointToObject(&te_ptr_to_cloned_obj);
>>> return
>>> RefCountingSmartPointer<std::remove_const_t<T>>(ref_copy,
>>> te_ptr_to_cloned_obj.typed_pointer);
>>> }
>>> };
>>>
>>> template<typename ...Types>
>>> struct SampleTypeList {};
>>>
>>> template<typename T>
>>> struct TypeErasedObject : TypelessObjectWithRefCounter
>>> {
>>> T obj;
>>>
>>> template<typename...Args>
>>> constexpr TypeErasedObject(Args&&... args)
>>> : obj(std::forward<Args>(args)...)
>>> {}
>>>
>>> constexpr TypelessObjectWithRefCounter* clone() const override;
>>>
>>> constexpr void SetTypelessToPointToObject(TypelessPointer* teptr)
>>> override
>>> {
>>> //using BaseList = typename std::bases<T>::type; // would like
>>> to do this
>>> using namespace TestClasses;
>>> using BaseList = SampleTypeList<A, B, C, D>; // hardcode
>>> typelist, since we don't have std::bases
>>>
>>> // this will go through the listed types (at runtime) and find
>>> the right one and set the pointer
>>> BaseListUnpacker<T, BaseList>::SetTyplessPointer(teptr,
>>> std::addressof(obj));
>>> }
>>> };
>>>
>>> inline constexpr TypelessObjectWithRefCounter* DoCloneRef(...)
>>> {
>>> //no copy constructor version
>>> return nullptr;
>>> }
>>>
>>> template<Copyable T>
>>> inline constexpr TypelessObjectWithRefCounter* DoCloneRef(const T& t)
>>> {
>>> return new TypeErasedObject<T>(t);
>>> }
>>>
>>> template<typename T>
>>> constexpr TypelessObjectWithRefCounter* TypeErasedObject<T>::clone()
>>> const
>>> {
>>> return DoCloneRef(obj);
>>> }
>>>
>>>
>>> /****** Provide a convenient interface for our smart pointer
>>> ******/
>>>
>>> template<typename T>
>>> using P = RefCountingSmartPointer<const T>;
>>>
>>> template<typename T>
>>> using Pm = RefCountingSmartPointer<T>;
>>>
>>> template<typename T, typename ...Args>
>>> constexpr Pm<T> Make(Args&&... args)
>>> {
>>> TypeErasedObject<T>* pteo = new
>>> TypeErasedObject<T>(std::forward<Args>(args)...);
>>> return Pm<T>(pteo, std::addressof(pteo->obj));
>>> }
>>>
>>>
>>> /****** Test main
>>> ******/
>>>
>>> int main()
>>> {
>>> using namespace TestClasses;
>>>
>>> P<A> pa = Make<A>();
>>> P<A> pb = Make<B>();
>>> P<A> pc = Make<C>();
>>> P<A> pd = Make<D>();
>>>
>>> Pm<A> pa_copy = pa.clone(); //works
>>> Pm<A> pb_copy = pb.clone(); //works
>>> Pm<A> pc_copy = pc.clone(); //works
>>> Pm<A> pd_copy = pd.clone(); //works
>>>
>>> P<C> pcd = Make<D>();
>>>
>>> Pm<C> pcd_copy = pcd.clone(); // also works
>>>
>>> pcd_copy->a = 42;
>>> pcd_copy->c = 137;
>>>
>>> std::cout << pcd->a << " = 0\n";
>>> std::cout << pcd->c << " = 2\n";
>>> std::cout << pcd->get() << " = 3\n";
>>>
>>> std::cout << pcd_copy->a << " = 42\n";
>>> std::cout << pcd_copy->c << " = 137\n";
>>> std::cout << pcd_copy->get() << " = 3\n";
>>> }
>>>
>>> On Tue, Jan 31, 2023 at 12:27 PM Arthur O'Dwyer <
>>> arthur.j.odwyer_at_[hidden]> wrote:
>>>
>>>>
>>>> Can you please show some example code? Just write the exact code you
>>>> think you want to write, but pretend that `std::bases_of<T>::type` already
>>>> exists. Show how you'll solve your problem using `std::bases_of<T>::type`.
>>>>
>>>> Thanks,
>>>> Arthur
>>>>
>>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>

Received on 2023-02-04 00:04:12