78

Both unique_ptr and shared_ptr accept a custom deleter to call on the object they own. But in the case of unique_ptr, the deleter is passed as a template parameter of the class, whereas the type of shared_ptr's custom deleter is to be specified as a template parameter of the constructor.

template <class T, class D = default_delete<T>> 
class unique_ptr
{
    unique_ptr(T*, D&); //simplified
    ...
};

and

template<class T>
class shared_ptr
{
    template<typename D>
    shared_ptr(T*, D); //simplified
    ...
};

I can't see why such difference. What requires that?

alter_igel
  • 6,899
  • 3
  • 21
  • 40
qdii
  • 12,505
  • 10
  • 59
  • 116
  • My guess is that the reason is that as an unique_ptr can only constructed once by one "person" while a shared_ptr might be used and passed around all over the programm and thus be constructed with different args. – Sebastian Hoffmann Jan 25 '14 at 19:19
  • 12
    `shared_ptr` type-erases the deleter, i.e. users of `shared_ptr` don't have to know what type the deleter has. This has a run-time cost (allocation, dereference), so it isn't performed for `unique_ptr` (which is overhead-free). E.g. see http://stackoverflow.com/q/6324694/420683 – dyp Jan 25 '14 at 19:19
  • @dyp: Maybe expand that into answer, including a brief tutorial on type erasure? Although it does not answer _why_ the same approach was not chosen for `unique_ptr`... Does it? – Nemo Jan 25 '14 at 19:22
  • 2
    @dyp Ok, but why does `shared_ptr` do that? – qdii Jan 25 '14 at 19:23
  • 1
    @Nemo `shared_ptr` has to store a bookkeeping object anyway, so it already requires an additional allocation (when not using `make_shared`). Also, it is sometimes useful to be able to use `shared_ptr`'s reference counting mechanism with pointers not under *the* shared ownership of the bookkeeping object. – dyp Jan 25 '14 at 19:23
  • 4
    @qdii `shared_ptr` implies shared ownership. It doesn't require of all owners to know how to destroy the object, and that's probably already good enough a reason to provide this type erasure. The overhead also isn't much larger because of the bookkeeping object. – dyp Jan 25 '14 at 19:28
  • 1
    @dyp I am a bit skeptical because C++ moto is "pay for what you use": type-erasing the destructor has a cost so there should be solid reason why it is needed. What do you mean by "it doesn't require of all owners to know how to destroy the object", isn't that the definition of shared-ownership that any of the owner might have to destroy the object? – qdii Jan 25 '14 at 19:37
  • 4
    @qdii IIRC there are implementations of `shared_ptr` that have virtually no overhead for the type erasure, as they just combine it with the overhead required for a bookkeeping object. -- Although any owner might be required to destroy the thing owned by the `shared_ptr`, the owner doesn't need to know *how* to do that, i.e. it doesn't need to see neither the definition nor the declaration of the release function of that owned thing. – dyp Jan 25 '14 at 19:39

2 Answers2

66

If you provide the deleter as template argument (as in unique_ptr) it is part of the type and you don't need to store anything additional in the objects of this type. If deleter is passed as constructor's argument (as in shared_ptr) you need to store it in the object. This is the cost of additional flexibility, since you can use different deleters for the objects of the same type.

I guess this is the reason: unique_ptr is supposed to be very lightweight object with zero overhead. Storing deleters with each unique_ptr could double their size. Because of that people would use good old raw pointers instead, which would be wrong.

On the other hand, shared_ptr is not that lightweight, since it needs to store reference count, so storing a custom deleter too looks like good trade off.

Wojtek Surowka
  • 20,535
  • 4
  • 44
  • 51
  • 1
    "you can use different deleters for objects of the same type": Do you mean that the standard wanted to allow two different `shared_ptr` pointing to the _same object_ to be destructed different ways? – qdii Jan 25 '14 at 19:26
  • *"If deleter is passed as constructor's argument (as in shared_ptr) you need to store it in the object"* Although it might not require additional storage; if the deleter is stateless it can be encoded in the type of the bookkeeping object. – dyp Jan 25 '14 at 19:26
  • 3
    @qdii - no, not the same object, but different shared_ptr of the same type. – Wojtek Surowka Jan 25 '14 at 19:28
  • 1
    @WojtekSurowka wait, if they are pointing to different objects of the same type T, passing the destructor as a class parameter is enough to let the user call different destructors for each of them. Just use `std::shared_ptr` for the first one, and `std::shared_ptr` for the second one. – qdii Jan 25 '14 at 19:32
  • 2
    @qdii: You need to imagine a function that takes a `shared_ptr` as argument, or an object that stores a `shared_ptr` as a member variable. Not having to care about the deleter makes them more flexible. – Nemo Jan 25 '14 at 19:34
  • Let's say I have a big system which uses shared_ptr and there are many functions accepting such pointers. If I introduce the second template argument, I would need either to make those functions templates, or to duplicate them. – Wojtek Surowka Jan 25 '14 at 19:35
  • @WojtekSurowka, @Nemo, true, but in the case of `unique_ptr` it didn’t bother anyone: if I have `foo( const std::unique_ptr& )`, then introducing the templated destructor brings the same problem: duplicate those functions or make them template. – qdii Jan 25 '14 at 20:00
  • 2
    It is all about tradeoffs. unique_ptr is not supposed to have any overhead, so the complication you described is a necessary cost. In the case of shared_ptr some overhead is already there, so it could be designed in more flexible way. – Wojtek Surowka Jan 25 '14 at 20:05
  • 1
    @qdii: You are not talking about `shared_ptr`s of the same type. Since they have different types, they can't be mixed in the same collection. – Ben Voigt Jan 25 '14 at 20:23
  • If `shared_ptr` took two template arguments you could still write `shared_ptr>`, so I don’t really see why this would be so good. –  Oct 08 '17 at 08:01
  • And that little type-erasure the `shared_ptr` does allows for doing some amazing things. I find that trade-off to be quite worthwhile all in all. – SirGuy Apr 24 '18 at 14:24
  • @WojtekSurowka Insightful answer. I forgot that template arguments actually "enhance" a type, giving it more nuances. "it is part of the type" Meaning that another `unique_ptr` without custom deleter wouldn't have the same type as the former right? – KeyC0de Oct 02 '18 at 07:56
3

Shared pointers of different types can share the ownership of the same object. See overload (8) of std::shared_ptr::shared_ptr. Unique pointers don't need such a mechanism, as they don't share.

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

If you didn't type-erase the deleter, you wouldn't be able to use such a shared_ptr<T, Y_Deleter> as a shared_ptr<T>, which would make it basically useless.

Why would you want such an overload?

Consider

struct Member {};
struct Container { Member member };

If you want to keep the Container alive, while you use the Member, you can do

std::shared_ptr<Container> pContainer = /* something */
std::shared_ptr<Member> pMember(pContainer, &pContainer->member);

and only have to hold onto pMember (perhaps put it into a std::vector<std::shared_ptr<Member>>)

Or alternatively, using overload (9)

template< class Y > 
shared_ptr( const shared_ptr<Y>& r ) noexcept; 
  // Only exists if Y* is implicitly convertible to T*

You can have polymorphic sharing

struct Base {};
struct Derived : Base {};

void operate_on_base(std::shared_ptr<Base>);

std::shared_ptr<Derived> pDerived = /* something*/
operate_on_base(pDerived);
Caleth
  • 52,200
  • 2
  • 44
  • 75