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.