In short: Use unique_ptr when you want a single pointer to an object that will be reclaimed when that single pointer is destroyed. Use shared_ptr when you want multiple pointers to the same resource.
this function is deprecated as of C++17 because use_count is only an approximation in multi-threaded environment.
So no, you shouldn't. The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership. So you shouldn't ever want to, either.
The smart pointer has an internal counter which is decreased each time that a std::shared_ptr , pointing to the same resource, goes out of scope – this technique is called reference counting. When the last shared pointer is destroyed, the counter goes to zero, and the memory is deallocated.
The trick is that std::shared_ptr
performs type erasure. Basically, when a new shared_ptr
is created it will store internally a deleter
function (which can be given as argument to the constructor but if not present defaults to calling delete
). When the shared_ptr
is destroyed, it calls that stored function and that will call the deleter
.
A simple sketch of the type erasure that is going on simplified with std::function, and avoiding all reference counting and other issues can be seen here:
template <typename T>
void delete_deleter( void * p ) {
delete static_cast<T*>(p);
}
template <typename T>
class my_unique_ptr {
std::function< void (void*) > deleter;
T * p;
template <typename U>
my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> )
: p(p), deleter(deleter)
{}
~my_unique_ptr() {
deleter( p );
}
};
int main() {
my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)
When a shared_ptr
is copied (or default constructed) from another the deleter is passed around, so that when you construct a shared_ptr<T>
from a shared_ptr<U>
the information on what destructor to call is also passed around in the deleter
.
shared_ptr<T>
logically[*] has (at least) two relevant data members:
The deleter function of your shared_ptr<Test>
, given the way you constructed it, is the normal one for Test
, which converts the pointer to Test*
and delete
s it.
When you push your shared_ptr<Test>
into the vector of shared_ptr<void>
, both of those are copied, although the first one is converted to void*
.
So, when the vector element is destroyed taking the last reference with it, it passes the pointer to a deleter that destroys it correctly.
It's actually a little more complicated than this, because shared_ptr
can take a deleter functor rather than just a function, so there might even be per-object data to be stored rather than just a function pointer. But for this case there is no such extra data, it would be sufficient just to store a pointer to an instantiation of a template function, with a template parameter that captures the type through which the pointer must be deleted.
[*] logically in the sense that it has access to them - they may not be members of the shared_ptr itself but instead of some management node that it points to.
It works because it uses type erasure.
Basically, when you build a shared_ptr
, it passes one extra argument (that you can actually provide if you wish), which is the deleter functor.
This default functor accepts as argument a pointer to type you use in the shared_ptr
, thus void
here, casts it appropriately to the static type you used test
here, and calls the destructor on this object.
Any sufficiently advanced science feels like magic, isn't it ?
The constructor shared_ptr<T>(Y *p)
indeed seems to be calling shared_ptr<T>(Y *p, D d)
where d
is an automatically generated deleter for the object.
When this happens the type of the object Y
is known, so the deleter for this shared_ptr
object knows which destructor to call and this information is not lost when the pointer is the stored in a vector of shared_ptr<void>
.
Indeed the specs require that for a receving shared_ptr<T>
object to accept a shared_ptr<U>
object it must be true that and U*
must be implicitly convertible to a T*
and this is certainly the case with T=void
because any pointer can be converted to a void*
implicitly. Nothing is said about the deleter that will be invalid so indeed the specs are mandating that this will work correctly.
Technically IIRC a shared_ptr<T>
holds a pointer to an hidden object that contains the reference counter and a pointer to the actual object; by storing the deleter in this hidden structure it's possible to make this apparently magic feature working while still keeping shared_ptr<T>
as big as a regular pointer (however dereferencing the pointer requires a double indirection
shared_ptr -> hidden_refcounted_object -> real_object
Test*
is implicitly convertible to void*
, therefore shared_ptr<Test>
is implicitly convertible to shared_ptr<void>
, from memory. This works because shared_ptr
is designed to control destruction at run-time, not compile-time, they will internally use inheritance to call the appropriate destructor as it was at allocation time.
I am going to answer this question (2 years later) using a very simplistic implementation of shared_ptr that the user will understand.
Firstly I am going to a few side classes, shared_ptr_base, sp_counted_base sp_counted_impl, and checked_deleter the last of which is a template.
class sp_counted_base
{
public:
sp_counted_base() : refCount( 1 )
{
}
virtual ~sp_deleter_base() {};
virtual void destruct() = 0;
void incref(); // increases reference count
void decref(); // decreases refCount atomically and calls destruct if it hits zero
private:
long refCount; // in a real implementation use an atomic int
};
template< typename T > class sp_counted_impl : public sp_counted_base
{
public:
typedef function< void( T* ) > func_type;
void destruct()
{
func(ptr); // or is it (*func)(ptr); ?
delete this; // self-destructs after destroying its pointer
}
template< typename F >
sp_counted_impl( T* t, F f ) :
ptr( t ), func( f )
private:
T* ptr;
func_type func;
};
template< typename T > struct checked_deleter
{
public:
template< typename T > operator()( T* t )
{
size_t z = sizeof( T );
delete t;
}
};
class shared_ptr_base
{
private:
sp_counted_base * counter;
protected:
shared_ptr_base() : counter( 0 ) {}
explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}
~shared_ptr_base()
{
if( counter )
counter->decref();
}
shared_ptr_base( shared_ptr_base const& other )
: counter( other.counter )
{
if( counter )
counter->addref();
}
shared_ptr_base& operator=( shared_ptr_base& const other )
{
shared_ptr_base temp( other );
std::swap( counter, temp.counter );
}
// other methods such as reset
};
Now I am going to create two "free" function called make_sp_counted_impl which will return a pointer to a newly created one.
template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
try
{
return new sp_counted_impl( ptr, func );
}
catch( ... ) // in case the new above fails
{
func( ptr ); // we have to clean up the pointer now and rethrow
throw;
}
}
template< typename T >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
return make_sp_counted_impl( ptr, checked_deleter<T>() );
}
Ok, these two functions are essential as to what will happen next when you create a shared_ptr through a templated function.
template< typename T >
class shared_ptr : public shared_ptr_base
{
public:
template < typename U >
explicit shared_ptr( U * ptr ) :
shared_ptr_base( make_sp_counted_impl( ptr ) )
{
}
// implement the rest of shared_ptr, e.g. operator*, operator->
};
Note what happens above if T is void and U is your "test" class. It will call make_sp_counted_impl() with a pointer to U, not a pointer to T. The management of the destruction is all done through here. The shared_ptr_base class manages the reference counting with regards to copying and assignment etc. The shared_ptr class itself manages the typesafe use of operator overloads (->, * etc).
Thus although you have a shared_ptr to void, underneath you are managing a pointer of the type you passed into new. Note that if you convert your pointer to a void* before putting it into the shared_ptr, it will fail to compile on the checked_delete so you are actually safe there too.
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