From http://en.cppreference.com/w/cpp/memory/unique_ptr:
If
T
is derived class (sic) of some baseB
, thenstd::unique_ptr<T>
is implicitly convertible tostd::unique_ptr<B>
. The default deleter of the resultingstd::unique_ptr<B>
will use operator delete forB
, leading to undefined behavior unless the destructor ofB
is virtual. Note thatstd::shared_ptr
behaves differently:std::shared_ptr<B>
will use the operator delete for the typeT
and the owned object will be deleted correctly even if the destructor ofB
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>
?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With