Alex Rivera | Logout

How does std::shared_ptr<void> call the correct object destructor?

Asked 2011-05-06T15:13:17.783
148

I found some code using std::shared_ptr<void> 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 following 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_ptr 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, and other implementations, actually break this code?

Edit
Report

2 Answers

121

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 an argument to the constructor, but if not present then it defaults to calling delete). When the shared_ptr is destroyed, it calls that stored deleter function that will call delete.

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 which destructor to call is also passed around in the deleter.

answered 2011-05-06T15:30:53.087
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.

answered 2011-05-06T15:30:05.720

Your Answer