Imagine a Deleter that has to stay with its object, as it is somewhat specific to its object. In my case, this is because the deleter uses an allocator library that needs to know the object size when deallocating the memory. Because of inheritance, I cannot simply use sizeof(T) but instead need to store the size of the derived object in the deleter on object creation.
template<typename T>
struct MyDeleter {
size_t objectSize;
MyDeleter() : objectSize(sizeof(T)) {}
template<typename S>
MyDeleter(const MyDeleter<S>& other) : objectSize(other.objectSize)
// object size is correctly transferred on assignment ^^^^^^^^^^^
{}
void operator(T* t) {
t->~T();
coolAllocatorLibrary::deallocate(t, objectSize);
// real object size needed for deletion ^^^^^^^^^^
}
}
template<typename T>
my_unique_ptr = std::unique_ptr<T, MyDeleter<T>>;
template<typename T, typename... Args>
my_unique_ptr <T> make_my_unique(Args&&... args) {
T* ptr = static_cast<T*>(coolAllocatorLibrary::allocate(sizeof(T)));
try {
new (ptr) T(std::forward<Args>(args)...);
} catch(...) {
coolAllocatorLibrary::deallocate(t, sizeof(T));
throw;
}
return my_unique_ptr <T>(ptr);
}
struct Base {};
struct Derived : Base {
uint64_t somePayloadToMakeThisClassBiggerThanItsParent;
}
This works nicely even in case of inheritance: I can safely delete an object of a derived class through a pointer of the super class, as long as the deleter is set correctly in the first place, which is guaranteed as long as make_my_unique is used:
{
my_unique_ptr<Base> ptr = make_my_unique<Derived>();
// Works fine. Here, even though ptr is of type <Base> it will deallocate
// correctly with sizeof(Derived)
}
The only problematic function is reset(), since I can use this function to put in a new pointer without also exchanging the deleter:
{
my_unique_ptr<Base> ptr = make_my_unique<Derived>();
ptr.reset(new Base()); // OUCH! This will delete the new Base() with sizeof(Derived)
}
So, is there any way with which I could make calling reset (with a non-nullptr) a compile-time error here? This would lead to a safe non-misuable interface.
Either my mental model is wrong, or this is a shortcoming of std::unique_ptr, because there doesn't seem to be a way to support this use case with a fully-safe interface.
I would imagine that there could be, e.g., some traits for the deleter where the deleter could disallow calling reset with a non-nullptr, but such things don't seem to exist.
I am aware of the "nuclear option" to just create a totally own my_unique_ptr class that has the actual std::unique_ptr in it and then exposes only the methods I want, but this is way more effort and it seems that object-specific allocators (e.g., PMRs) should be supported by std::unique_ptr.