What is the purpose of std::enabled_shared_from_this<T>?

Last year, as I was messing around with async networking using Boost.Asio, I faced this class from the header <memory>. And I asked the most beloved question of a C++ developer: “WTF is this?!?”.
Today I again started working on a project that uses asio and decided on explaining what is going on since I have a blog now.

The famous std::shared_ptr

In order to understand std::enable_shared_from_this, one must understand how a shared pointer works.
And if you think you know how a shared pointer works but don’t know what std::enable_shared_from_this does, you should definitely not skip this section.

A std::shared_ptr<T> is a smart pointer that counts the reference of a wrapped raw pointer and frees the memory whenever that memory count reaches 0.
It is especially useful for scenarios where you need to share some resource among different objects. A shared pointer wraps the two following pointers:

(1) Member variables of the shared_ptr_base class

I won’t go into the details but these pointers are basically our raw pointer and a pointer to the control block (even though they commented only as “Reference counter”, it does much more than that). The control block also contains the reference counter of our std::shared_ptr<T>. Below is another diagram to understand how it looks like because this structure is very important to not mess the things up when working with shared pointers.

(2) Structure of a shared_ptr

Grasping this concept is very important because as the diagram suggests, the raw pointer and the control block do not know each other. This is the source of many possible pitfalls, which I will cover in the next section.

What could go wrong?

Let’s dive into a basic usage of shared pointer to understand what might go wrong:

1
2
3
auto data = new MyClass;

std::shared_ptr<MyClass> s_ptr1 = std::shared_ptr<MyClass>(data);

This is a basic case to create a raw pointer and pass the ownership of this data to the shared pointer.
From now on, the shared pointer will care about the deallocation of the data. At this stage the memory of this shared pointer looks like:

(3) State of the s_ptr1

Now let’s make things more exciting and create another shared pointer that shares the ownership.
For this purpose I will show two different cases. First we will cover the good case and second the bad case. Consider the following first case:

1
2
3
4
auto data = new MyClass;

std::shared_ptr<MyClass> s_ptr1 = std::shared_ptr<MyClass>(data);
std::shared_ptr<MyClass> s_ptr2 = s_ptr1;

In this case we do the exact same thing in the last step but eventually we create another shared pointer by using the copy constructor of the std::shared_ptr<T>.
Now it results in a new std::shared_ptr<MyClass> object which shares the ownership by pointing to the same object and to the control block.
The current state now looks like:

(4) State of the s_ptr1 and s_ptr2. RefCounter is now 2

As a result, the reference count is equal to 2. This way we can make sure that our data is available for s_ptr1 even if s_ptr2 goes out of the scope and vice versa.

Now let’s consider another possible implementation, where things might not work as expected:

1
2
3
4
auto data = new MyClass;

std::shared_ptr<MyClass> s_ptr1 = std::shared_ptr<MyClass>(data);
std::shared_ptr<MyClass> s_ptr2 = std::shared_ptr<MyClass>(data);

In this code instead of using the copy constructor of the std::shared_ptr<T>, we simply pass the raw pointer to the constructor.
In the last section I mentioned an important take. The data and the control block have no idea about our shared pointer.
Therefore if we just take the raw pointer and construct another shared pointer it results in:

(5) State of the s_ptr1 and s_ptr2. RefCounter is separate for each shared pointer

As we see in the diagram (5), now each shared pointer has its own reference counter while pointing to the same data.
That results in an ambiguity and a possible time bomb.
Whoever first goes out of scope will release the data and leave the other one to point to an invalid memory address.
Now if you are using address sanitizer and such a case happens in your tests, you are lucky. If not without specific knowledge it is painful to address the issue.

The magic of std::enable_shared_from_this<T>

Now lets get back to the main topic. Assume that you want to generate a new shared pointer within the class itself.
A reason for that might be generating a new shared pointer and passing it as a message to some other component or saving the object as a shared pointer into a container.
Here is a very basic example code which can lead to the pitfall mentioned above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
std::shared_ptr<MyClass> getSharedPtr() {
return std::shared_ptr<MyClass>(this); // Creates a new control block
}
};

int main() {
std::shared_ptr<MyClass> s_ptr1 = std::make_shared<MyClass>();

std::shared_ptr<MyClass> s_ptr2 = s_ptr1->getSharedPtr();

// ----> Destructor called twice!
}

We are trying to create a new shared pointer from the current object. The approach is naive but wrong.
The simple reason is that we are creating a new shared pointer by passing the raw pointer to the shared pointer constructor.
However, this leads to an undefined behaviour.

In order to solve that issue and generate a std::shared_ptr<MyClass> that shares the same reference counter as the initial object, we use the std::enable_shared_from_this<T>.
The correct implementation looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getSharedPtr() {
return shared_from_this(); // Uses the same control block as the current object
}
};

int main() {
std::shared_ptr<MyClass> s_ptr1 = std::make_shared<MyClass>(); // --> RefCount == 1

std::shared_ptr<MyClass> s_ptr2 = s_ptr1->getSharedPtr(); // --> RefCount == 2 as intended

// ---> Destructor called only once since both of the objects go out of scope
}

In this case we inherit our class from std::enable_shared_from_this<T> and use the function shared_from_this to create a new shared pointer that points to the same control block as the current object.

You might think that this would not be hard to catch since the examples are straightforward. However, I tried to keep the examples as simple as possible.
In larger projects and codebases that kind of errors might take days or weeks to catch.
Therefore I think it is always important to understand how the most basic things work in order to fix the most complex problems. Happy Coding!

Author

Mert Asil Türeli

Posted on

2024-12-18

Updated on

2025-01-18

Licensed under