C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Base class reflection

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Wed, 1 Feb 2023 13:46:57 -0500
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 (...) {}

    }

};

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

Received on 2023-02-01 18:47:12