Modern (Effective) C++ - Prefer to use convenience functions when creating unique or shared pointers.

This article is inspired by Item 21 of Effective Modern C++ by Scott Meyers.
C++11 provides a function to create shared_ptr called std::make_shared, The similar version for unique pointer was included in C++14 called std::make_unique.

If you want to use make_unique functionality in C++11 project, here is a quick and dirty version

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
view raw make_unique.cpp hosted with ❤ by GitHub

There are three distinct advantages of using the make functions -

1. Removing the necessity to mention the type twice, leading to more compact code.

//Using new
std::unique_ptr<int> uPtr(new int(21)); // notice that int is repeated twice
//Using std::make_unique
auto uPtr(std::make_unique<int>(22));
//Using std::make_shared
auto sPtr(std::make_shared<int>(23));
2. Making sure that memory is released even when an exception is raised. In the below example, there is a possibility that objects are not freed when exception is thrown.

#include <iostream>
#include <memory>
using namespace std;
const int startSpecialTaskNo = 5;
const int endSpecialTaskNo = 8;
class A {
public:
static int initial;
int id;
A()
{
id = initial++;
std::cout << "Constructing object with id: " << id << '\n';
}
~A()
{
std::cout << "Destructing object with id: " << id << "\n\n";
}
};
int A::initial = 1;
int computeTaskNumber()
{
static int i = 0;
++i;
cout << "task number = " << i << '\n';
if (i >= startSpecialTaskNo && i <= endSpecialTaskNo)
{
throw std::exception("Special task numbers are not allowed for general use\n");
}
return i;
}
void funcA(std::shared_ptr<A> sp, int taskNo)
{
// do some work using sp and taskNo
cout << "do some work\n";
}
int main()
{
int totalTasks = 10;
for (int i = 1; i <= totalTasks; ++i)
{
try
{
funcA(std::shared_ptr<A>(new A()), computeTaskNumber());
}
catch (std::exception& e)
{
std::cout << e.what() << '\n';
}
}
char ch;
cin >> ch;
}
The output of this program in my laptop is -
In the above example we can use that objects of class A is getting constructed and destructed from id 1 to id 4 ( inclusive), but from task 5 to 8, the object are not getting destructed. Notice I am throwing an exception for these.

The sequence of operations here is
1. Call new A() to create new object of A.
2. Call computeTaskNumber ( which throws exception from 5 to 8)
3. Assign the newly created object to the shared_ptr ( which never gets called due to the exception thrown above).

Hence the memory for objects 5 to 8 lead to the infamous memory leak

However if we use, std::make_shared, we can always be sure that if the object is constructed, then it is destroyed as well.

The only change in the below program is on line number 51, where I have called make_shared instead of using new.

#include <iostream>
#include <memory>
using namespace std;
const int startSpecialTaskNo = 5;
const int endSpecialTaskNo = 8;
class A {
public:
static int initial;
int id;
A()
{
id = initial++;
std::cout << "Constructing object with id: " << id << '\n';
}
~A()
{
std::cout << "Destructing object with id: " << id << "\n\n";
}
};
int A::initial = 1;
int computeTaskNumber()
{
static int i = 0;
++i;
cout << "task number = " << i << '\n';
if (i >= startSpecialTaskNo && i <= endSpecialTaskNo)
{
throw std::exception("Special task numbers are not allowed for general use\n");
}
return i;
}
void funcA(std::shared_ptr<A> sp, int taskNo)
{
// do some work using sp and taskNo
cout << "do some work\n";
}
int main()
{
int totalTasks = 10;
for (int i = 1; i <= totalTasks; ++i)
{
try
{
funcA(std::make_shared<A>(), computeTaskNumber());
}
catch (std::exception& e)
{
std::cout << e.what() << '\n';
}
}
char ch;
cin >> ch;
}
The output of the program is -





We see that objects 5 to 8 never gets created.
The above scenario is same for the usage of std::make_unique as well.

3. This advantage is only for std::shared_ptr.

When we use new , there are two calls for the memory manager. One to create the object, and another call to create the control block.
However if we use std::make_shared, only a single call is made to allocate enough space for both object and the control block.

According to Optimized C++, a single call to the memory manager translates to thousands of CPU instructions. Hence we save a significant amount of time, by making only a single call for memory allocation.

The downside of this occurs when we are use weak pointers in our project.

In this scenario,
  • The object is created using make_shared, hence the memory is allocated for both the object and the control block.
  • There are currently weak pointers pointing to this shared_ptr. ( weak_ref_count > 0)
  • All shared pointers to this object gets destroyed. ( shared_ref_count becomes 0)
  • The object gets destroyed.
  • Since weak reference count is greater than 1, we cannot free the control block.
  • But since the memory is allocated using make_shared ( which allocates space for both object and control block), we can not free up the space of the deleted raw object.
  • The memory is only reclaimed once all the weak pointers are destroyed (weak ref counter = 0).

However if we had used new, the memory corresponding to the object could have been reclaimed as soon as shared_ref_count hit 0.

On a final note, we cannot use make_shared or make_unique when we are using custom deleters. To circumvent the problem we can write the code like this -

std::shared_ptr<A> aObj(new A(), deleterFunc);
funcA(aObj, computeTaskNumber());
However this is a bit inefficient, since when the shared_ptr is sent to funcA, the reference count is manipulated atomically. So we can modify our code in the following fashion -

std::shared_ptr<A> aObj(new A(), deleterFunc);
funcA(std::move(aObj), computeTaskNumber());
std::move converts lvalue to rvalue object (reference count is not manipulated). aObj then becomes nullptr.

PS: There is another version of shared pointer called std::allocate_shared, which uses custom allocators. More about it in this link.
Modern (Effective) C++ - Prefer to use convenience functions when creating unique or shared pointers. Modern (Effective) C++ - Prefer to use convenience functions when creating unique or shared pointers. Reviewed by zeroingTheDot on February 06, 2018 Rating: 5

No comments:

Powered by Blogger.