Friday, May 8, 2015

The shared_ptr aliasing constructor

The shared_ptr aliasing constructor is one of the more obscure corners of C++11. It allows you to create a shared_ptr that points to one object, but owns another. The pointed to object is what you get from operator*, operator->, etc.; the owned object is the one that is deleted when the reference count reaches zero.

Why would you want to separate these two things?

Use case 1


The motivating case was a shared_ptr that owns an instance of some complex class, but points to one of its sub-objects. For example:

struct Bar {};

struct Foo
{
   Bar bar;
};

class OwnsBar
{
private:
   const std::shared_ptr<Bar> m_bar;

public:
   OwnsBar(const std::shared_ptr<Bar>& bar)
      : m_bar(bar)
   {
   }
};

std::shared_ptr<OwnsBar> CreateBarOwner()
{
   auto foo = std::make_shared<Foo>();
   std::shared_ptr<Bar> aliasBarPtr(foo, &foo->bar);
   return std::make_shared<OwnsBar>(aliasBarPtr);
}

Here, aliasBarPtr owns the entire Foo struct and keeps it alive, but it points to the Bar member. This means that (a) aliasBarPtr can be used anywhere a shared_ptr<Bar> is expected, and (b) aliasBarPtr will keep the entire parent Foo alive even after the local foo variable goes out of scope.

Use case 2


Another use for the aliasing constructor is when some (badly-designed, most likely) function wants a std::shared_ptr<Bar>, but the Bar you have isn't in a shared_ptr:

void f(const std::shared_ptr<Bar>& bar);

void g(Bar& bar)
{
   // don't actually do this unless you have no choice; see comments below
   std::shared_ptr<Bar> fakeSharedPtrBar(std::shared_ptr<Bar>(), &bar);
   f(fakeSharedPtrBar);
}

fakeSharedPtrBar owns nothing, but it points to bar. In effect, it is a raw pointer masquerading as a shared_ptr.

Note that pulling this little sleight of hand is dangerous; if f passes the fake shared_ptr to someone else who hangs onto it beyond the point that g's Bar is destroyed, things will blow up. Only use this hack if you're absolutely sure f won't do this and refactoring it to take a raw pointer or reference isn't an option.

Use case 3


One more use for the aliasing constructor is to tie object lifetimes together. For example, suppose you have a shared_ptr<Document> and two ambient shared_ptr<IDocumentObservers> that you want to keep alive for exactly as long as the Document stays around. The aliasing constructor can help:

// Utility function to relocate an object into a shared_ptr using its move constructor
// obj will no longer be valid upon returning
template<typename T>
std::shared_ptr<T> MoveIntoSharedPtr(T& obj)
{
   return std::make_shared<T>(std::move(obj));
}

std::shared_ptr<Document> CreateDocument()
{
   auto doc = std::make_shared<Document>();

   auto observer1 = std::make_shared<Observer1>();
   doc->AddWeakObserver(std::weak_ptr<IDocumentObserver>(observer1));

   auto observer2 = std::make_shared<Observer2>();
   doc->AddWeakObserver(std::weak_ptr<IDocumentObserver>(observer2));

   auto keepAliveTuple = std::make_tuple(doc, observer1, observer2);

   return std::shared_ptr<Document>(MoveIntoSharedPtr(keepAliveTuple), doc.get());
}

In a previous post, I used a custom deleter to achieve this same thing. That works almost all the time, but you can get odd bugs if the type in question (Document, in the example above) implements enable_shared_from_this. See this stackoverflow question for details.

No comments:

Post a Comment