C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Base class reflection

From: Billy Martin <bmartin_at_[hidden]>
Date: Tue, 31 Jan 2023 18:53:29 -0800
/* 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 02:53:42