Why do std::shared_ptr<void> work

C++C++11Shared Ptr

C++ Problem Overview


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?

C++ Solutions


Solution 1 - C++

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.

Solution 2 - C++

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.

Solution 3 - C++

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 ?

Solution 4 - C++

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

Solution 5 - C++

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.

Solution 6 - C++

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLiKaoView Question on Stackoverflow
Solution 1 - C++David Rodríguez - dribeasView Answer on Stackoverflow
Solution 2 - C++Steve JessopView Answer on Stackoverflow
Solution 3 - C++Matthieu M.View Answer on Stackoverflow
Solution 4 - C++6502View Answer on Stackoverflow
Solution 5 - C++PuppyView Answer on Stackoverflow
Solution 6 - C++CashCowView Answer on Stackoverflow