Why do std::shared_ptr<void> work

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:

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


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 ?