it seems to me there are two problem domains:
- DSDL types and runtime introspection
- Concrete object lifecycle in libcyphal
This post only discusses item (2) and a technique for erasing platform-specific, concrete types from objects vended through libcyphal.
I’m working on the first version of libcyphal that we’ll ask for an in-depth design review on and I have the following idea for shielding the transport layer from types defined for a specific platform.
And so I give you … the Voldemort Pointer?
/// TODO integrate this functionality into cetl::pmr::Factory
struct VoldemortPointer final
{
template <typename T>
using allocator_t = cetl::pmr::polymorphic_allocator<T>;
template <typename T>
using unique_ptr_t = std::unique_ptr<T, cetl::pmr::PolymorphicDeleter<allocator_t<T>>>;
/// Construct a new concrete type but return a unique_ptr to an interface type for the concrete object.
/// This becomes a polymorphic "voldemort" of sorts where only RTTI provides a safe way to refer to the
/// concrete type after construction.
template <typename InterfaceType, typename ConcreteType, typename... Args>
static unique_ptr_t<InterfaceType> make_unique(allocator_t<ConcreteType>& concrete_allocator, Args&&... args)
{
ConcreteType* s = concrete_allocator.allocate(1);
if (s)
{
#if __cpp_exceptions
try
{
concrete_allocator.construct(s, std::forward<Args>(args)...);
} catch (...)
{
concrete_allocator.deallocate(s, 1);
throw;
}
#else
concrete_allocator.construct(s, std::forward<Args>(args)...);
#endif
}
typename unique_ptr_t<InterfaceType>::deleter_type td{allocator_t<InterfaceType>{concrete_allocator.resource()},
1};
return unique_ptr_t<InterfaceType>{s, td};
}
VoldemortPointer() = delete;
};
for example:
#include "CatToy.hpp"
#include "VoldemortPointer.hpp"
struct ICat
{
virtual void meow() const noexcept = 0;
protected:
// Yes, this is a wart. You could make the dtor public but then anyone could delete it.
// This, at least, prevents a casual mistake and has the added benefit of describing
// the one place in the system to look for who is going to delete a cat (soul-less bastards!)
friend class cetl::pmr::PolymorphicDeleter<cetl::pf17::pmr::polymorphic_allocator<ICat>>;
~ICat() = default;
};
...
using CatPointer = VoldemortPointer::unique_ptr_t<ICat>;
extern CatPointer make_cat(const CatToy&);
… then, elsewhere we implement make_cat
…
class TomMarvoloRiddle: public ICat
{
public:
TomMarvoloRiddle(const CatToy& toy) : toy_{toy} {}
virtual ~TomMarvoloRiddle() = default;
void meow() const noexcept override
{
// implement meow.
}
private:
CatToy toy_;
};
cetl::pf17::pmr::polymorphic_allocator<TomMarvoloRiddle> voldemort_allocator_{cetl::pf17::pmr::new_delete_resource()};
CatPointer make_cat(const CatToy& toy)
{
VoldemortPointer::make_unique<ICat, TomMarvoloRiddle>(voldemort_allocator_, toy);
}
This technique takes advantage of the polymorphic part of the polymorphic_allocator
in that the deleter is able to use polymorphism to delete the concrete type without a need to refer to the concrete type (i.e. the concrete type is erased in the pointer). This technique also avoids auto
or hiding anything other than the object’s vtbl
which is the sole source of dark magic in this configuration.
Okay, true, this isn’t very novel (and the Voldemort reference is contrived) but it’s subtly different from just passing up pointers and deleting through base class types in that the concrete allocator doesn’t need to be much of an allocator at all. In fact, our TomMarvoloRiddle
object may be an item in a static array where we are using a monotonic_buffer_resource
or could even be a singleton where we provide a no-op deleter. This pattern completely separates memory management of the concrete type from the APIs that vend references to its interfaces. Furthermore, ownership is transferred to the caller of make_unique
and the factory context does not maintain any access to the concrete type.