std::unique_ptr::reset and object-specific deleters

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.

Answer

The unique_ptr has a member type pointerwhich is equal to std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*. Must satisfy NullablePointer. So you may try to add a pointer type to your deleter like this:

template<typename T>
struct MyDeleter {
   struct pointer{
       using type = T;
       pointer():_ptr(nullptr){}
       pointer(std::nullptr_t):_ptr(nullptr){}

       //this is required to support creating uniqu_ptr of base class
       template <typename U, typename = std::enable_if_t<std::is_base_of_v<T, typename U::type>, void>>
       pointer(U ptr):_ptr(ptr){}

       T* operator->(){
           return _ptr;
       }

       operator T*() const{
           return _ptr;
       }

    private:
        T *_ptr;
        friend struct MyDeleter;
        explicit pointer(T* ptr):_ptr(ptr){}
   };

   size_t objectSize;

   MyDeleter() : objectSize(sizeof(T)) {}

   template<typename S>
   MyDeleter(const MyDeleter<S>& other) : objectSize(other.objectSize)
  {}

   void operator()(pointer t) {
       t->~T();
       deallocate(t, objectSize); 
   }

   static pointer make_ptr(T* ptr){
       return pointer{ptr};
   }
};

template<typename T>
using 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*>(allocate(sizeof(T)));
   try {
      new (ptr) T(std::forward<Args>(args)...);
   } catch(...) {
      deallocate(ptr, sizeof(T));
      throw;
   }
   return my_unique_ptr <T>(MyDeleter<T>::make_ptr(ptr));
}

The main idea is to prevent the construction of this pointer wrapper outside of the deleted class. Given this code we will have the following:

struct Base {
    virtual ~Base()=default;
    virtual void foo(){std::cout<<"foo"<<std::endl;}
    int bar{0};
};
struct Derived : Base { 
    uint64_t somePayloadToMakeThisClassBiggerThanItsParent; 
};
struct Derived2 : Base { 
    uint64_t somePayloadToMakeThisClassBiggerThanItsParent; 
    void foo(){std::cout<<"foo2"<<std::endl;}
};

int main(){
    my_unique_ptr<Base> ptr = make_my_unique<Derived>(); //works
    ptr.reset(); //works
    ptr.reset(nullptr); //works
    ptr =  make_my_unique<Derived2>(); //works;
    ptr->foo(); //works
    ptr->bar=2; //works
    *ptr = Base{}; //works if a copy constructor of Base is available
    ptr.reset(new Base()); //does not work. cannot create a pointer wrapper
    ptr.reset(new Derived()); //does not work. cannot create a pointer wrapper. Even with a exact type but created with new.
}