shared_ptr aliasing constructor
Wednesday, April 25th, 2012One of the interesting C++11 additions to std::shared_ptr
compared to TR1 is the aliasing constructor (also available in boost::shared_ptr
since 1.35.0). This constructor allows us to create a shared_ptr
that shares ownership of one object but points to another. The signature of this constructor looks like this:
template <class Y> shared_ptr (const shared_ptr<Y>& r, T* p) noexcept;
The first argument (r
) is the pointer with which we will share ownership of object Y
. While the second argument (p
) is the object which we will actually point to. That is, get()
and operator->
will return p
, not r
. In fact, to understand this better, it is useful to think of shared_ptr
as consisting of two parts: the object that it owns (or, more precisely, shares ownership of) and the object that it stores. When we use other shared_ptr
constructors, these two objects are the same (give or take base-derived differences). The aliasing constructor allows us to create a shared pointer that has different objects in these two parts. Note also that the stored object is never deleted by shared_ptr
. If a shared pointer created with the aliasing constructor goes out of scope, and it is the last pointer owning r
, then r
is deleted, not p
.
What can the aliasing constructor be useful for? Because the stored object is never deleted by shared_ptr
, to avoid the possibility of dangling pointers, we need to make sure that the lifetime of the stored object is at least as long as that of the owned object. The two primary arrangements that meet this requirement are data members in classes and elements in containers. Passing a pointer to a data member while ensuring the lifetime of the containing object will probably be the major use-case of the aliasing constructor. Here are a few examples:
struct data {...}; struct object { data data_; }; void f () { shared_ptr<object> o (new object); // use_count == 1 shared_ptr<data> d (o, &o->data_); // use_count == 2 o.reset (); // use_count == 1 // When d goes out of scope, object is deleted. } void g () { typedef std::vector<object> objects; shared_ptr<objects> os (new objects); // use_count == 1 os->push_back (object ()); os->push_back (object ()); shared_ptr<object> o1 (os, &os->at (0)); // use_count == 2 shared_ptr<object> o2 (os, &os->at (1)); // use_count == 3 os.reset (); // use_count == 2 // When o1 goes out of scope, use_count becomes 1. // When o2 goes out of scope, objects is deleted. }
While the above examples are synthetic, here is a real-world case, taken from ODB, an ORM for C++. In ODB, when one needs to save an object to or load it from a database, it is done using the database
class. Underneath, the database
class has a database connection factory which can have different implementations (e.g, a pool or a connection per thread). Sometimes, however, one may need to perform a low-level operation which requires accessing the connection directly instead of going through the database
interface. To support this, the database
class provides a function which returns a connection. The tricky part is to make sure the connection does not outlive the factory that created it. This would be bad, for example, if a connection tried to return itself to the connection pool that has already been deleted. The aliasing constructor allows us to solve this quite elegantly:
class connection {...}; class connection_factory {...}; class database { ... database (const std::shared_ptr<connection_factory>&); std::shared_ptr<connection> connection () { return std::shared_ptr<connection> ( factory_, factory_->connection ()); } private: std::shared_ptr<connection_factory> factory_; };
While there is no aliasing constructor for weak_ptr
, we can emulate one by first creating shared_ptr
:
shared_ptr<object> o (new object); shared_ptr<data> d (o, &o->data_); weak_ptr<data> wd (d);
At first it may seem that passing around aliased weak_ptr
is the same as passing a raw pointer. However, weak_ptr
has one major advantage: we can check if the pointer is still valid and also make sure that the object is not deleted while we are working with it:
if (shared_ptr<data> d = wd.lock ()) { // wd is still valid and we can safely use data // as long as we hold d. }
Let’s now look at some interesting special cases that are made possible with the aliasing constructor. Remember that without the aliasing constructor we can only create shared pointers that own and store the same object. If, for example, we initialize a shared pointer with nullptr
, then both the owned and stored objects will be NULL
. With the aliasing constructor, however, it is possible to have one NULL
while the other non-NULL
.
Let’s start with the case where the owned object is NULL
while the stored one is not. This is perfectly valid, although a bit strange; the use_count
will be 0 while get()
will return a valid pointer. What can something like this be useful for? One interesting use-case that I could think of is to turn a shared pointer into essentially a raw pointer. This could be useful, for example, if an interface expects a shared pointer but in some special cases we need to pass, say, a statically allocated object which shall never be deleted. Continuing with the ODB example, if we are using a connection per thread factory, it doesn’t make sense to have more than one instance of this factory in an application. So we might as well allocate it statically:
class connection_per_thread_factory {...}; static connection_per_thread_factory cpt_factory_; static std::shared_ptr<connection_factory> cpt_factory ( std::shared_ptr<connection_factory> (), &cpt_factory_); void f () { database db (cpt_factory); }
Note also that while the same can be achieved by providing a no-op deleter, the aliasing constructor approach has an advantage of actually not performing any reference counting, which can be expensive because of the atomicity requirement.
The other special case is where the stored object is NULL
while the owned one is not. In fact, we can generalize this case by observing that the stored value doesn’t really have to be a pointer since all shared_ptr
does with it is copy it around and return it from get()
. So, more generally, shared_ptr
can be made to store any value of the size_t
width. It can be 0, some flag, counter, index, timestamp, etc.
What can we use this for? Here is one idea: Let’s say our application works with a set of heterogeneous objects but we only want some limited number of them to ever be present in the application’s memory. Say, they can be loaded from the database, if and when needed. So what we need is some kind of cache that keeps track of all the objects already in memory. When a new object needs to be loaded, the cache finds the oldest object in memory and purges it (i.e., the FIFO protocol).
Here is how we can implement this using the aliasing constructor. Our cache will be the only place in the application holding shared pointers to the object. Except instead of storing a pointer to the object, we will store a timestamp in shared_ptr
. Other parts of our application will all hold weak pointers to the objects they are working with. Before accessing the object, they will lock weak_ptr
to check if the object is still in memory and to make sure it will not be unloaded while being used. If the weak pointer is not valid, then the application asks the cache to load it. Here is an outline of this cache implementation:
class fifo_cache { public: template <class T> std::weak_ptr<T> load (unsigned long obj_id) { // Remove the oldest object from objects_. std::shared_ptr<T> o (/* load object given its id */); size_t ts (/* generate timestamp */); std::shared_ptr<void> x ( o, reinterpret_cast<void*> (ts)); objects_.push_back (x); return std::weak_ptr<T> (o); } private: std::vector<std::shared_ptr<void>> objects_; };
If you know of any other interesting uses for these two special cases, do share in the comments.