5
\$\begingroup\$

There is a basic difference in the way C++ manages the deleter for std::unique_ptr and std::shared_ptr, mainly for allowing unique_ptr with default deleter to have the same size as a raw pointer.

However, the way std::unique_ptr manages its deleter requires stating the deleter type, in case you need a custom deleter, and also doesn't allow an easy way for setting a deleter of a different type for a declared unique_ptr (so for example you cannot manage a container of unique_ptrs with different type of deleters).

This leads people to use shared_ptr in cases where there is a need for different custom deleters, even if there is no actual need for shared ownership.

Proposing below a polymorphic_deleter that easily allows std::unique_ptr to have true polymorphic behavior for its deleter, similar to the way shared_ptr works.

Features of the suggested code:

  • no change of std::unique_ptr is required
  • no need to declare the type of the deleter
  • can use the same unique_ptr with different custom deleters, the proper deleter would be called based on the runtime type
  • allows assignment of unique_ptr with default deleter into unique_ptr with polymorphic_deleter, i.e. std::default_delete is considered as a type of polymorphic_deleter
  • the size of a unique_ptr with polymorphic_deleter is the size of a raw pointer + size of std::function (e.g. 40 bytes if pointer is 8 bytes and std::function is 32)

class base_polymorphic_deleter for a common base

The class doesn't have a virtual destructor in purpose, as we do not delete it using a pointer to base

class base_polymorphic_deleter {
protected:
    // the deleter with type erased using std::function
    std::function<void(void*)> deleter;
    base_polymorphic_deleter(std::function<void(void*)>&& deleter)
    : deleter { std::move(deleter) } {}
public:
    auto get_deleter() {
        return deleter;
    }
};

template class polymorphic_deleter<P> for actual deleters

Using template parameter so the type can check that the actual deleter passed to it matches the managed pointer type.

The code relies on the type traits code from: https://github.com/kennytm/utils/blob/master/traits.hpp to validate in compile time that the provided deleter matches the managed pointer type.

template<typename P>
class polymorphic_deleter: public base_polymorphic_deleter {
    // private ctor
    polymorphic_deleter(std::function<void(void*)>&& deleter)
    : base_polymorphic_deleter { std::move(deleter) } {}
    // private validator for compile time checking arg type against pointer type
    template<typename ARG,
      std::enable_if_t<std::is_same<ARG, P>() || std::is_base_of<ARG, P>()>*
      dummy = nullptr>
    void check_arg_matching_pointer(){}
    // end of private part

Constructors for polymorphic_deleter:

public:
  // empty ctor for default polymorphic_deleter
  polymorphic_deleter()
  : base_polymorphic_deleter {
      [](void* p) { 
        delete static_cast<P*>(p);
      }
  } {}
  // custom polymorphic_deleter
  template<typename F>
  polymorphic_deleter(F&& method)
  : base_polymorphic_deleter {
    [inner_deleter = std::forward<F>(method)](void* p) mutable {
        inner_deleter(static_cast< ARG1<F> >(p));
    }
  } {
    using ARG = typename std::remove_pointer< ARG1<F> >::type;
    check_arg_matching_pointer<ARG>();        
  }
  // getting another polymorphic deleter
  template<typename PP>
  polymorphic_deleter(polymorphic_deleter<PP>&& another)
  : base_polymorphic_deleter { std::move(another.get_deleter()) } {
    check_arg_matching_pointer<PP>();    
  }    
  // getting std::default_delete
  template<typename PP>
  polymorphic_deleter(std::default_delete<PP>&& defaultDeleter)
  : base_polymorphic_deleter {
    [inner_deleter = std::forward<std::default_delete<P>>(defaultDeleter)]
    (void* p) mutable {
        inner_deleter(static_cast<PP*>(p));
    }
  } {}

Assignment:

    template<typename PP>
    polymorphic_deleter& operator=(polymorphic_deleter<PP>&& another) {
        deleter = std::move(another.get_deleter());
        return *this;
    }

operator():

    void operator() (void* p) { 
        std::cout << "in polymorphic_deleter for address: " << p << std::endl;
        std::cout << "calling the actual deleter..." << std::endl;
        deleter(p);
    }
};
// end of polymorphic_deleter

Utilities:

template<typename T, typename... Args>
std::unique_ptr<T, polymorphic_deleter<T>>
make_unique_with_default_polymorphic_deleter(Args&&... args) {
    return {
      new T(std::forward<Args>(args)...),
      polymorphic_deleter<T>{}
    };
}

template<typename T, typename F, typename... Args>
std::unique_ptr<T, polymorphic_deleter<T>>
make_unique_with_polymorphic_deleter(F&& deleter, Args&&... args) {
    return { 
      new T(std::forward<Args>(args)...),
      polymorphic_deleter<T>{ std::forward<F>(deleter)}
    };
}

template<typename T>
using unique_ptr_with_polymorphic_deleter =
   std::unique_ptr<T, polymorphic_deleter<T>>;

Usage Example

// Intuitively:
// IntegerDerived is derived from Integer
// IntegerDerivedDeleter and IntegerDeleter are their deleters
// for full code see link below
int main() {
    auto pint = make_unique_with_polymorphic_deleter<Integer>(IntegerDeleter{});
    pint = std::make_unique<Integer>(1);
    pint = make_unique_with_default_polymorphic_deleter<IntegerDerived>(2);
    pint = std::make_unique<IntegerDerived>(3);
    pint = std::make_unique<Integer>(4);
    pint = std::make_unique<IntegerDerived>(5);
    pint = make_unique_with_default_polymorphic_deleter<Integer>(6);
}

Code: http://coliru.stacked-crooked.com/a/3ffc35f71ab6c432

Comments on the need of this class (alternatives?) and on the implementation would be welcomed.

\$\endgroup\$
3
  • \$\begingroup\$ I'm not sure whether this is correct (hence comment, not answer), but can we actively prevent deletion of base_polymorphic_deleter as base class by giving it a protected default deleter? (i.e. protected: ~base_polymorphic_deleter() = default;) \$\endgroup\$ Commented Sep 27, 2019 at 9:47
  • 1
    \$\begingroup\$ Do you have a use-case for those polymorphic deleters? std::unique_ptr<T, std::function<void(T*)>> already covers a lot of polymorphic cases and the ones it doesn't cover, like std::unique_ptr<Base, BaseDel> p = std::unique_ptr<Derived, DerivedDel>{}; look questionable. \$\endgroup\$
    – nwp
    Commented Sep 27, 2019 at 12:09
  • \$\begingroup\$ @nwp this is how unique_ptr works with custom deleters without the proposed code: coliru.stacked-crooked.com/a/7e3d158b890b8067 the deleter is not polymorphic even if it has virtual methods as can be seen in the code. \$\endgroup\$
    – Amir Kirsh
    Commented Sep 27, 2019 at 15:07

0