Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the rationale for the difference in destruction behavior between std::unique_ptr and std::shared_ptr?

Tags:

c++

c++14

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>?

like image 375
Ton van den Heuvel Avatar asked Feb 19 '15 20:02

Ton van den Heuvel


1 Answers

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.

like image 102
Yakk - Adam Nevraumont Avatar answered Oct 27 '22 00:10

Yakk - Adam Nevraumont