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@gmail.com> 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@gmail.com> 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