This blog post assumes that you already have an understanding of the normal use case of a std::shared_ptr
, namely managing the lifetime of a shared object that is being used from multiple locations.
Imagine you have a configuration object in your project that you hold via a std::shared_ptr
.
For instance, like so:
struct HeavyObject
{
// a lot of expensive members here
};
struct Config
{
HeavyObject heavy_{};
// other stuff
};
auto configPtr{std::make_shared<Config>()};
Code language: C++ (cpp)
Now assume that you need to pass the HeavyObject
member of the Config
object to some other function within your project. One way to do it would be to pass the configPtr and thus the entire Config
object. But this would make your code coupled to the Config
class even though you only need to know the HeavyObject
class. Since this would lead to tighter coupling and at the same time violate the Law of Demeter, this cannot be the right approach.
Next, you might be thinking of passing the HeavyObject
member via HeavyObject*
, HeavyObject&
, or std::reference_wrapper<HeavyObject>
to the function. This could work but it is potentially dangerous because you have to guarantee that the Config
object is still alive when accessing the HeavyObject
instance via the reference. If its lifetime has already ended, your pointer or reference will be dangling, and accessing it is UB.
In order to tackle this problem, one can use the aliasing constructor of std::shared_ptr
.
This is what it looks like for a std::shared_ptr<T>
:
template<typename U>
std::shared_ptr<T>( const std::shared_ptr<U>& r, T* ptr ) noexcept;
Code language: C++ (cpp)
In order to understand it, we have to have a look at how a std::shared_ptr
is usually implemented.
Every std::shared_ptr
has basically two raw pointers as members. Let’s call the first one storedPtr
and the second one controlPtr
.
The storedPtr
points to a T
. It is the pointer which is returned by the std::shared_ptr
when calling the get()
member function. It is the pointer that gets dereferenced when dereferencing the std::shared_ptr
via ->
or *
.
The controlPtr
points to a ControlBlock
which is needed for managing the lifetime of the T
. The ControlBlock
contains a pointer to the T
whose lifetime is being handled; let’s call this pointer managedPtr
.
In the normal use case of a std::shared_ptr
, the storedPtr
and managedPtr
point to the same T
instance. In other words, the object that you access via the std::shared_ptr
is the same object whose lifetime is being handled by the std::shared_ptr
. This is depicted in figure 1.
But this is only a special case. The most general case is depicted in figure 2.
The std::shared_ptr
points in this case to a T
instance but manages the lifetime of a U
instance. T
and U
are in general different types. This configuration can be achieved by using the aliasing constructor of std::shared_ptr
.
So, back to our problem: We want to create a std::shared_ptr
that points to a HeavyObject
instance within a Config
instance. Additionally, it needs to manage the lifetime of the Config
instance. This would allow accessing the HeavyObject
instance while ensuring that the Config
instance is still alive. This is depicted in figure 3.
This can be coded like so:
assert(configPtr.use_count() == 1); // Config object is kept alive due to one shared_ptr managing it
std::shared_ptr<HeavyObject> heavyPtr{configPtr, &configPtr->heavy_};
assert(configPtr.use_count() == 2); // heavyPtr is also managing Config object's lifetime, thus use_count increased by one
assert(heavyPtr.use_count() == configPtr.use_count()); // since they share the control block, the use count is the same
assert(heavyPtr.get() == &configPtr->heavy_); // heavyPtr points to heavy_ in the guts of Config instance
Code language: C++ (cpp)
This concludes the blog post. Now you know how to use std::shared_ptr’s aliasing constructor to safely pass around members of objects without triggering UB due to lifetime issues. In case you found this informative, you can read the next blog post, where you can learn more peculiar facts about the std::shared_ptr
‘s aliasing constructor.