How it works...

Although they should be avoided, circular references are likely to occur as your projects grow more and more complex and in size. If shared smart pointers are leveraged when these circular references occur, a hard to find memory leak can occur. To understand how this is possible, let's look at the following example:

class car;
class engine;

As shown in the preceding code, we start with two class prototypes. Circular references almost always start in this fashion as one class depends on another and vice versa, requiring the use of a class prototype.

Let's define a car as follows:

class car
{
friend void build_car();
std::shared_ptr<engine> m_engine;

public:
car() = default;
};

As shown in the preceding code, this is a simple class that stores a shared pointer to an engine and friends a function named build_car(). Now, we can define an engine as follows:

class engine
{
friend void build_car();
std::shared_ptr<car> m_car;

public:
engine() = default;
};

As shown in the preceding code, an engine is similar to a car with the difference that the engine stores a shared pointer to a car. Both, however, friend a build_car() function. Both also create default constructed shared pointers, meaning their shared pointers are NULL pointers at the time of construction.

The build_car() function is used to complete the construction of each object, as follows:

void build_car()
{
auto c = std::make_shared<car>();
auto e = std::make_shared<engine>();

c->m_engine = e;
e->m_car = c;
}

As shown in the preceding code, we create each object and then set the car's engine and vice versa. Since both the car and the engine are scoped to the build_car() function, we expect that these pointers will be deleted once the build_car() function returns. Now, we can execute this build_car() function as follows:

int main(void)
{
build_car();
return 0;
}

This seems like a simple program, but it has a hard to find memory leak. To demonstrate this, let's run this application in valgrind, which is a dynamic memory analysis tool that's capable of detecting memory leaks:

As shown in the preceding screenshot, valgrind says that memory was leaked. If we run valgrind with --leak-check=full, it will tell us that the memory leaks are with the car and engine shared pointers. The reason this memory leak occurs is that the car holds a shared reference to an engine. This same engine holds a shared reference to the car itself.

For example, consider the following code:

void build_car()
{
auto c = std::make_shared<car>();
auto e = std::make_shared<engine>();

c->m_engine = e;
e->m_car = c;

std::cout << c.use_count() << ' ';
std::cout << e.use_count() << ' ';
}

As shown in the preceding code, we have added a call to use_count(), which outputs the number of owners std::shared_ptr contains. If this is executed, we'll see the following output:

The reason we can see two owners is because the build_car() function holds a reference to a car and an engine here:

    auto c = std::make_shared<car>();
auto e = std::make_shared<engine>();

The car holds a second reference to an engine because of this:

    c->m_engine = e;

This is also the same for the engine and the car. When the build_car() function completes, the following loses scope first:

    auto e = std::make_shared<engine>();

The engine, however, is not deleted because the car still holds a reference to the engine. Then, the car loses scope:

    auto c = std::make_shared<car>();

However, the car is not deleted because the engine (which hasn't been deleted yet) also holds a reference to the car. This results in build_car() returning with neither the car nor the engine being deleted because both still hold a reference to each other, with no means of telling either object to remove their references.

This type of circular memory leak, although easy to identify in our example, can be extremely difficult to identify in complex code, which is one of many reasons why shared pointers and circular dependencies should be avoided (usually a better design can remove the need for both). If this cannot be avoided, std::weak_ptr can be used instead, as follows:

class car
{
friend void build_car();
std::shared_ptr<engine> m_engine;

public:
car() = default;
};

As shown in the preceding code, we still define our car as holding a shared reference to an engine. We do this as we assume a car has a longer lifetime (that is, in our model, you can have a car without an engine, but you cannot have an engine without a car). The engine, however, is defined as follows:

class engine
{
friend void build_car();
std::weak_ptr<car> m_car;

public:
engine() = default;
};

As shown in the preceding code, the engine now stores a weak reference to the car. Our build_car() function is defined as follows:

void build_car()
{
auto c = std::make_shared<car>();
auto e = std::make_shared<engine>();

c->m_engine = e;
e->m_car = c;

std::cout << c.use_count() << ' ';
std::cout << e.use_count() << ' ';
}

As shown in the preceding code, the build_car() function doesn't change. The difference now is that, when we execute this application using valgrind, we see the following output:

As shown in the preceding screenshot, there are no memory leaks, and the use_count() for the car is 1, while the use_count() for the engine is still 2 compared to the previous example. In the engine class, we use std::weak_ptr, which has access to the managed object std::shared_ptr manages, but doesn't increase the managed object's internal count when created. This provides std::weak_ptr with the ability to query whether std::shared_ptr is valid without having to hold a strong reference to the pointer itself.

The reason the memory leak is removed is that, when the engine loses scope, its use count is decreased from 2 to 1. Once the car loses scope, which only has a use count of 1, it gets deleted, which in turn decrements the engine's use count to 0, which causes the engine to be deleted as well.

The reason we use std::weak_ptr instead of a C-style pointer in the engine is because std::weak_ptr provides us with the ability to query the managed object to see if the pointer is still valid. For example, suppose we need to check whether the car still exists, as follows:

class engine
{
friend void build_car();
std::weak_ptr<car> m_car;

public:
engine() = default;

void test()
{
if (m_car.expired()) {
std::cout << "car deleted ";
}
}
};

Using the expired() function, we can test to see whether the car still exists before using it, which is something that isn't possible with a C-style pointer. Now, we can write our build_car() function as follows:

void build_car()
{
auto e = std::make_shared<engine>();

{
auto c = std::make_shared<car>();

c->m_engine = e;
e->m_car = c;
}

e->test();
}

In the preceding example, we create an engine and then create a new scope that creates our car. Then, we create our circular reference and lose scope. This causes the car to be deleted as expected. The difference is that our engine isn't deleted yet as we still hold a reference to it. Now, we can run our test function, which results in the following output when it's run with valgrind:

As shown in the preceding screenshot, there are no memory leaks. std::weak_ptr successfully removed the chicken and egg problem that was introduced by the circular reference. As a result, std::shared_ptr is able to function as expected, releasing memory in the right order. In general, circular references and dependencies should be avoided whenever possible, but, if they cannot be avoided, std::weak_ptr, as shown in this recipe, can be used to prevent memory leaks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset