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 intounique_ptr
withpolymorphic_deleter
, i.e.std::default_delete
is considered as a type ofpolymorphic_deleter
- the size of a
unique_ptr
withpolymorphic_deleter
is the size of a raw pointer + size ofstd::function
(e.g. 40 bytes if pointer is 8 bytes andstd::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.
base_polymorphic_deleter
as base class by giving it a protected default deleter? (i.e.protected: ~base_polymorphic_deleter() = default;
) \$\endgroup\$std::unique_ptr<T, std::function<void(T*)>>
already covers a lot of polymorphic cases and the ones it doesn't cover, likestd::unique_ptr<Base, BaseDel> p = std::unique_ptr<Derived, DerivedDel>{};
look questionable. \$\endgroup\$