C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Base class reflection

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Fri, 3 Feb 2023 10:13:56 +0100
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-03 09:14:10