2

Is there a reason behind why std::unique_ptr contains the deleter function signature as part of the template definition?

template<class T, class Deleter = std::default_delete<T>> class unique_ptr;

vs

template< class T > class shared_ptr;
David G
  • 94,763
  • 41
  • 167
  • 253
lcs
  • 4,227
  • 17
  • 36
  • Those two have different semantics beyond merely the difference in `Deleter`. [See also](http://stackoverflow.com/a/6876833/489590) – Brian Cain Apr 14 '14 at 16:53
  • One reason is if it wasn't a template parameter you would need to carry it around as a member variable. – Captain Obvlious Apr 14 '14 at 16:54
  • 1
    @CaptainObvlious: You still have to carry the object around (although it can be eliminated if it contains no state). If it wasn't a template parameter, you'd need to use type erasure to support different types - so you'd always have to carry a pointer around even if it has no state. – Mike Seymour Apr 14 '14 at 16:56
  • 2
    The way `shared_ptr` works requires it to store some data on the heap. Since it already has that, it's able to type-erase the deleter. `unique_ptr` has no such overhead to hide the deleter in, so it's required to know the type at all times. – Mooing Duck Apr 14 '14 at 16:56
  • 1
    If you want a type-erased deleter on your unique_ptr you can just use `std::unique_ptr< T, std::function< void(T*) > >` since a deleter is just a callable object that takes a pointer. – Mikael Persson Apr 14 '14 at 17:07

4 Answers4

11

The general objective of unique_ptr is to provide automatic deletion of a pointer when the unique-pointer goes out of scope. When the default deleter is used (just calls delete), there is no need for any extra data member in a unique_ptr object except for the pointer itself. This means that by default, unique_ptr has virtually no overhead whatsoever (since most, if not all, of its functions will be inlined).

But they also wanted to be able to provide the option to change the deleter, for the special circumstances where it makes sense. The only way to provide that option, while still being able to optimize it away (storage and inlining the calls) is to make it a static part of the type itself, i.e., through a template parameter. The point is that unique_ptr is intended to be a minimal overhead alternative for a raw pointer.

In the case of shared_ptr, the objective is quite different and the existing overhead is too. A shared-pointer actually uses a shared storage (dynamically allocated) where it stores the pointer, the reference count and the deleter object. In other words, there is already significant overhead and an appropriate place to put the deleter object without causing extra per-pointer overhead. Furthermore, with all the reference counting mechanisms, the overhead of a virtual call (to perform the deletion) pales in comparison to the existing overhead. That's why it was a natural choice to include that convenient feature of type-erasing the deleter object in the shared-pointers.

And, if you want to create a unique-pointer type that has a type-erased deleter, it is quite simple to do with a template alias:

template <typename T>
using any_unique_ptr = std::unique_ptr< T, std::function< void(T*) > >;

Or something similar along those lines, like with this deleter:

template <typename T>
struct type_erased_delete {
  std::function< void(T*) > f;

  type_erased_delete() : f(std::default_delete<T>()) { };

  template <typename Func>
  type_erased_delete(Func&& aF) : f(std::forward<Func>(aF)) { };

  void operator()(T* p) const { f(p); };
};

template <typename T>
using any_unique_ptr = std::unique_ptr< T, type_erased_delete<T> >;
Mikael Persson
  • 18,174
  • 6
  • 36
  • 52
3

It means that, if the deleter has no state, the unique_ptr can be no larger than a plain pointer; and moving it no more expensive than copying and nulling a pointer.

To be able to handle multiple types of deleter, as shared_ptr can, it would need to use type erasure. To do that, each unique_ptr would need to contain a pointer to the deleter (to call it polymorphically), as well as a pointer to the managed object, doubling its size.

shared_ptr already has that extra overhead, since it needs a pointer to access the reference count. The deleter can be stored along with the reference count.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
3

In the case of shared_ptr, it uses type-erasure on the deleter. There is some extra overhead in performing that type erasure, which unique_ptr does not want. In particular, most solutions will require allocation of dynamic memory for any function object deleter. Given the goal of unique_ptr, I can understand why they chose not to use that technique.

On the other hand, shared_ptr already includes some extra overhead for the reference counting, as it will already require dynamic allocation for the reference count. This means that the additional overhead for the type-erasure was smaller. That, and that's how boost::shared_ptr was written, and the standardized version was basically a capture of that.

Dave S
  • 20,507
  • 3
  • 48
  • 68
0

Could be constructed with new[] - Therefore delete[] would need to be called - not delete.

The template gives you this option along with things such as logging etc.

Enlico
  • 23,259
  • 6
  • 48
  • 102
Ed Heal
  • 59,252
  • 17
  • 87
  • 127