C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Base class reflection

From: Billy Martin <bmartin_at_[hidden]>
Date: Wed, 1 Feb 2023 20:33:03 -0800
Arthur, THANK YOU for your suggestions.

When I was thinking about this problem, I had thought about using a virtual
member function in TypelessPointer, and I even thought about passing a
function argument to it somehow, but I couldn't get around the fact that
you had to put both types in the signature, somewhere, regardless. I hadn't
thought of using throw/catch to do the typed conversion across two separate
functions. That's disgusting. I will definitely use it as my workaround for
not having std::bases, meanwhile, because it's better than the very dirty
workaround I had been using up until now.

However, you're wrong about one thing: It does need to be constexpr
everywhere, or at least it very much wants to be, especially for a smart
pointer class. This allows me to do compile-time unit tests, which I've
been trying to use in my coding more, lately. Unfortunately, your
throw/catch trick is not constexpr compatible.

I will admit that I was probably a bit aggressive in putting virtual base
classes in my test case, which are also not constexpr compatible.

Here is my updated example, with constexpr testing, and using the tr2
version of std::bases in gcc, to show that it works and is constexpr
compatible. This is in the off chance that you guys think the position of
the standards committee will be, "what? no, we're not going to add that
simple reflection mechanism to the standard, you should be abusing the
exception system to do that sort of thing, like everybody else. Begone!"

https://godbolt.org/z/7Wf91nTv5

Though it occurs to me that maybe, just maybe, this sort of type conversion
should be doable in a simpler manner. Without exceptions.

Billy

On Wed, Feb 1, 2023 at 10:47 AM Arthur O'Dwyer <arthur.j.odwyer_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 (...) {}
>
> }
>
> };
>
> 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-02 04:33:17