Why are two raw pointers to the managed object needed in std::shared_ptr implementation?

The reason for this is that you can have a shared_ptr which points to something else than what it owns, and that is by design. This is implemented using the constructor listed as nr. 8 on cppreference:

template< class Y >
shared_ptr( const shared_ptr<Y>& r, T *ptr );

A shared_ptr created with this constructor shares ownership with r, but points to ptr. Consider this (contrived, but illustrating) code:

std::shared_ptr<int> creator()
{
  using Pair = std::pair<int, double>;

  std::shared_ptr<Pair> p(new Pair(42, 3.14));
  std::shared_ptr<int> q(p, &(p->first));
  return q;
}

Once this function exits, only a pointer to the int subobject of the pair is available to client code. But because of the shared ownership between q and p, the pointer q keeps the entire Pair object alive.

Once dealloacation is supposed to happen, the pointer to the entire Pair object must be passed to the deleter. Hence the pointer to the Pair object must be stored somewhere alongside the deleter—in other words, in the control block.

For a less contrived example (probably even one closer to the original motivation for the feature), consider the case of pointing to a base class. Something like this:

struct Base1
{
  // :::
};

struct Base2
{
  // :::
};

struct Derived : Base1, Base2
{
 // :::
};

std::shared_ptr<Base2> creator()
{
  std::shared_ptr<Derived> p(new Derived());
  std::shared_ptr<Base2> q(p, static_cast<Base2*>(p.get()));
  return q;
}

Of course, the real implementation of std::shared_ptr has all the implicit conversions in place so that the p-and-q dance in creator is not necessary, but I’ve kept it there to resemble the first example.

Leave a Comment