std::unique_ptr::reset 和特定于对象的删除器

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

想象一个 Deleter 必须与其对象保持一致,因为它在某种程度上特定于其对象。在我的例子中,这是因为删除器使用了一个分配器库,在释放内存时需要知道对象的大小。由于继承,我不能简单地使用 sizeof(T) 而是需要在对象创建时将派生对象的大小存储在删除器中。

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; 
}

即使在继承的情况下也能很好地工作:只要删除器在第一名,只要使用make_my_unique就可以保证:

{
   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)  
}

唯一有问题的函数是 reset(),因为我可以使用这个函数放入一个新的指针而无需同时交换删除器:

{
   my_unique_ptr<Base> ptr = make_my_unique<Derived>();
   ptr.reset(new Base()); // OUCH! This will delete the new Base() with sizeof(Derived)
}

那么,有什么方法可以让我在这里调用 reset(使用非 nullptr)时出现编译时错误?这将导致一个安全的不可误用的界面。 要么我的心智模型有误,要么这是 std::unique_ptr 的缺点,因为似乎没有办法用完全安全的接口来支持这个用例。

我想可能会有一些删除器的特征,删除器可能不允许使用非 nullptr 调用重置,但这样的事情似乎不存在。

我知道“核选项”只是创建一个完全自己的 my_unique_ptr class,其中包含实际的 std::unique_ptr,然后仅公开我想要的方法,但这需要更多的努力,并且似乎 std::unique_ptr.

应该支持特定于对象的分配器(例如 PMR)

unique_ptr 有一个成员类型 pointer,如果该类型存在则等于 std::remove_reference<Deleter>::type::pointer,否则 T*。必须满足 NullablePointer。 所以你可以尝试像这样向你的删除器添加一个 pointer 类型:

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));
}

主要思想是防止在删除的class之外构造这个指针包装器。鉴于此代码,我们将具有以下内容:

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.
}