What is the rationale for the difference in destruction behavior between std::unique_ptr and std::shared_ptr?
c++, c++14
Solution
`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.
Problem
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>`?