7

From http://en.cppreference.com/w/cpp/memory/unique_ptr:

If T is derived class (sic) of some base B, then std::unique_ptr<T> is implicitly convertible to std::unique_ptr<B>. The default deleter of the resulting std::unique_ptr<B> will use operator delete for B, leading to undefined behavior unless the destructor of B is virtual. Note that std::shared_ptr behaves differently: std::shared_ptr<B> will use the operator delete for the type T and the owned object will be deleted correctly even if the destructor of B is not virtual.

What is the rationale for the difference in behavior upon destruction that is described above? My initial guess would be performance?

Also interesting to know is how an std::shared_ptr<B> is able to call the destructor of a type T in case the destructor on B is non-virtual and can not be called as far as I can see from the context of std::shared_ptr<B>?

Ton van den Heuvel
  • 10,157
  • 6
  • 43
  • 82
  • 1
    This might help: http://stackoverflow.com/questions/6876751/differences-between-unique-ptr-and-shared-ptr – Eldad Mor Feb 19 '15 at 20:29

1 Answers1

8

std::shared_ptr<X> already has a bunch of overhead over a raw B*.

A shared_ptr<X> basically maintains 4 things. It maintains a pointer-to-B, it maintains two reference counts (a "hard" reference count, and a "soft" one for weak_ptr), and it maintains a cleanup function.

The cleanup function is why shared_ptr<X> behaves differently. When you create a shared_ptr<X>, a function that calls that particular type's destructor is created and stored in the cleanup function managed by the shared_ptr<X>.

When you change types managed (B* becomes C*), the cleanup function remains unchanged.

Because shared_ptr<X> needs to manage the reference counts, the extra overhead of that cleanup function storage is marginal.

For a unique_ptr<B>, the class is almost as cheap as a raw B*. It maintains zero state other than its B*, and its behavior (at destruction) boils down to if (b) delete b;. (Yes, that if (b) is redundant, but an optimizer can figure that out).

In order to support cast-to-base and delete-as-derived, extra state would have to be stored that remembers the unique_ptr is really to a derived class. This could be in the form of a stored pointer-to-deleter, like a shared_ptr.

That would, however, double the size of a unique_ptr<B>, or require it to store data on the heap somewhere.

It was decided that unique_ptr<B> should be zero-overhead, and as such it doesn't support cast-to-base while still calling base's destructor.

Now, you can probably teach unique_ptr<B> to do this by simply adding a deleter type and storing a destruction function that knows the type of thing it is destroying. The above has been talking about the default deleter of unique_ptr, which is stateless and trivial.

struct deleter {
  void* state;
  void(*f)(void*);
  void operator()(void*)const{if (f) f(state);}
  deleter(deleter const&)=default;
  deleter(deleter&&o):deleter(o) { o.state = nullptr; o.f=nullptr; }
  deleter()=delete;
  template<class T>
  deleter(T*t):
    state(t),
    f([](void*p){delete static_cast<T*>(p);})
  {}
};
template<class T>
using smart_unique_ptr = std::unique_ptr<T, deleter>;

template<class T, class...Args>
smart_unique_ptr<T> make_smart_unique( Args&&... args ) {
  T* t = new T(std::forward<Args>(args)...);
  return { t, t };
}

live example, where I generate a unique-ptr to derived, store it in a unique-ptr to base, and then reset base. The derived pointer is deleted.

( A simple void(*)(void*) deleter might run into problems whereby the passed in void* would differ in value between the base and derived cases. )

Note that changing the pointer stored in such a unique_ptr without changing the deleter will result in ill advised behavior.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Good answer, but `unique_ptr` is not really zero-overhead in the general case. It has to store a deleter too. Unlike `shared_ptr`, though, the deleter type is part of its type (through a template parameter). For zero-size deleters, zero-overhead is indeed possible through empty base optimisation. – Angew is no longer proud of SO Feb 19 '15 at 20:49
  • @Angew I always forget if it does the if test or not. I do think I've seen an implementation that does the wrong thing there. – Yakk - Adam Nevraumont Feb 19 '15 at 21:03
  • @AaronMcDaid I have added a smarter unique ptr, simply using the 2nd parameter deleter, that stores what is to-be-deleted. It has way more overhead than a `unique_ptr`. – Yakk - Adam Nevraumont Feb 19 '15 at 21:04
  • The `if(b)` part is important for custom deleters that can't handle nulls. Also, a `std::function` is a bad deleter - its constructors can throw. – T.C. Feb 21 '15 at 03:17
  • @T.C. agreed. Light weight two-pointer `deleter` written. Do not think we can do this stateless, because determining how to delete `derived` from `base` basically requires a second pointer. Hmm, maybe through careful use sub-typed deleters, where `unique_ptr< base, deleter >` records `void* -> base -> derived` then delete? Basically keep track of how the cast-to-base changed the `void*` and unroll. Tricky. – Yakk - Adam Nevraumont Feb 22 '15 at 01:20
  • @BenVoigt it had fewer `` clauses at the time angew commented. I fixed it in response to his comment. – Yakk - Adam Nevraumont Feb 22 '15 at 02:57
  • Aside: am now convinced that stateless is not just tricky, but impossible in general. – Yakk - Adam Nevraumont Feb 22 '15 at 02:58