138

I found some code using std::shared_ptr to perform arbitrary cleanup at shutdown. At first I thought this code could not possibly work, but then I tried the following:

#include <memory>
#include <iostream>
#include <vector>

class test {
public:
  test() {
    std::cout << "Test created" << std::endl;
  }
  ~test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>" 
            << std::endl;
  std::vector<std::shared_ptr<void>> v;
  {
    std::cout << "Creating test" << std::endl;
    v.push_back( std::shared_ptr<test>( new test() ) );
    std::cout << "Leaving scope" << std::endl;
  }
  std::cout << "Leaving main" << std::endl;
  return 0;
}

This program gives the output:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

I have some ideas on why this might work, that have to do with the internals of std::shared_ptrs as implemented for G++. Since these objects wrap the internal pointer together with the counter the cast from std::shared_ptr<test> to std::shared_ptr<void> is probably not hindering the call of the destructor. Is this assumption correct?

And of course the much more important question: Is this guaranteed to work by the standard, or might further changes to the internals of std::shared_ptr, other implementations actually break this code?

Zhen
  • 4,171
  • 5
  • 38
  • 57
LiKao
  • 10,408
  • 6
  • 53
  • 91

6 Answers6

113

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.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • There seems to be a misprint: `my_shared`. I'd fix that but have no privilege to edit yet. – Alexey Kukanov May 06 '11 at 15:50
  • @Alexey Kukanov, @Dennis Zickefoose: Thanks for the edit I was away and did not see it. – David Rodríguez - dribeas May 07 '11 at 08:53
  • in fact, you don't even need a `std::function` (which leads one to wonder how *it* works); a simple function pointer is sufficient – user102008 Nov 15 '11 at 23:48
  • 2
    @user102008 you don't need ‘std::function‘ but it is a bit more flexible (probably does not matter here at all), but that does not change how type erasure works, if you store ‘delete_deleter‘ as the function pointer ‘void ( void* )‘ you are performing the type erasure there: T is gone from the stored pointer type. – David Rodríguez - dribeas Nov 16 '11 at 10:05
  • @DavidRodríguez-dribeas I've posted a [similar question](http://stackoverflow.com/questions/22654422/using-stdshared-ptrvoid-to-point-to-anything) just now, it would be great if you could help! – Bruce Mar 26 '14 at 10:08
  • 1
    This behavior is guaranteed by the C++ standard, right? I need type erasure in one of my classes, and `std::shared_ptr` lets me avoid declaring a useless wrapper class just so that I could inherit it from a certain base class. – Violet Giraffe Dec 23 '14 at 08:19
  • @DavidRodríguez-dribeas Sir I'm little noobie with type erasure thing, can you please point me where exactly type erasure thing going on in your code? Thanks – Angelus Mortis Jan 16 '16 at 07:24
  • 1
    @AngelusMortis: The exact deleter is not part of the type of `my_unique_ptr`. When in `main` the template is instantiated with `double` the right deleter is chosen *but* this does not part of the type of `my_unique_ptr` and cannot be retrieved from the object. The type of the deleter is *erased* from the object, when a function receives a `my_unique_ptr` (say by rvalue-reference), that function does not and needs not know what the deleter is. – David Rodríguez - dribeas Jan 16 '16 at 19:09
38

shared_ptr<T> logically[*] has (at least) two relevant data members:

  • a pointer to the object being managed
  • a pointer to the deleter function that will be used to destroy it.

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 deletes 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.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • 4
    +1 for mentioning that the deleter function/functor is copied into other shared_ptr instances - a piece of info missed in other answers. – Alexey Kukanov May 06 '11 at 15:47
  • Does this mean that virtual base destructors is not needed when using shared_ptrs? – ronag May 06 '11 at 15:52
  • @ronag Yes. However, I'd still recommend making the destructor virtual, at least if you have any other virtual members. (The pain of accidentally forgetting once outweighs any possible benefit.) – Alan Stokes May 06 '11 at 16:06
  • Yes, I would agree. Interesting non-the-less. I knew about type erasure just hadn't considered this "feature" of it. – ronag May 06 '11 at 16:17
  • 3
    @ronag: virtual destructors are not required if you create the `shared_ptr` directly with the appropriate type or if you use `make_shared`. But, still it is a good idea as the type of the pointer can change from construction until it is stored in the `shared_ptr`: `base *p = new derived; shared_ptr sp(p);`, as far as `shared_ptr` is concerned the object is `base` not `derived`, so you need a virtual destructor. This pattern can be common with factory patterns, for example. – David Rodríguez - dribeas May 07 '11 at 08:59
11

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 ?

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
5

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.

CashCow
  • 30,981
  • 5
  • 61
  • 92
5

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
6502
  • 112,025
  • 15
  • 165
  • 265
4

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.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • Can you explain more? I've posted a similar question just now, it would be great if you could help! – Bruce Mar 26 '14 at 10:02