We will write a program that shows us how unique_ptr handles memory by creating a custom type that adds some debug messages upon its construction and destruction. Then, we will play around with unique pointers, maintaining dynamically allocated instances of it:
- First, we include the necessary headers and declare that we use the std namespace:
#include <iostream>
#include <memory>
using namespace std;
- We are going to implement a little class for the object we are going to manage using unique_ptr. Its constructor and destructor print to the terminal, so we can see later when it is actually automatically deleted:
class Foo
{
public:
string name;
Foo(string n)
: name{move(n)}
{ cout << "CTOR " << name << 'n'; }
~Foo() { cout << "DTOR " << name << 'n'; }
};
- In order to see what limitations a function has that accepts unique pointers as arguments, we just implement one. It processes a Foo item by printing its name. Note that while unique pointers are smart, overhead-free, and comfortably safe, they can still be null. This means that we still have to check them before we dereference them:
void process_item(unique_ptr<Foo> p)
{
if (!p) { return; }
cout << "Processing " << p->name << 'n';
}
- In the main function, we will open another scope, create two Foo objects on the heap, and manage both with unique pointers. We create the first one explicitly on the heap using the new operator and then put it into the constructor of the unique_ptr<Foo> variable, p1. We create the unique pointer, p2, by calling make_unique<Foo> with the arguments we would otherwise directly give the constructor of Foo. This is the more elegant way because we can use auto type deduction and the first time we can access the object, it is already managed by unique_ptr:
int main()
{
{
unique_ptr<Foo> p1 {new Foo{"foo"}};
auto p2 (make_unique<Foo>("bar"));
}
- After we left the scope, both objects are destructed immediately and their memory is released to the heap. Let's have a look at the process_item function and how to use it with unique_ptr now. If we construct a new Foo instance, managed by a unique_ptr in the function call, then its lifetime is reduced to the scope of the function. When process_item returns, the object is destroyed:
process_item(make_unique<Foo>("foo1"));
- If we want to call process_item with an object that already existed before the call, then we need to transfer ownership because that function takes a unique_ptr by value, which means that calling it would lead to a copy. But unique_ptr cannot be copied, it can only be moved. Let's create two new Foo objects and move one into process_item. By looking at the terminal output later, we will see that foo2 is destroyed when process_item returns because we transferred ownership to it. foo3 will continue living until the main function returns:
auto p1 (make_unique<Foo>("foo2"));
auto p2 (make_unique<Foo>("foo3"));
process_item(move(p1));
cout << "End of main()n";
}
- Let's compile and run the program. At first, we see the constructor and destructor calls of foo and bar. They are indeed destroyed just after the program leaves the additional scope. Note that the objects are destroyed in the opposite order of their creation. The next constructor line comes from foo1, which is the item we created during the process_item call. It is indeed destroyed immediately after the function call. Then we created foo2 and foo3. foo2 is destroyed immediately after the process_item call where we transferred the ownership. The other item, foo3, in comparison, is destroyed after the last code line in the main function:
$ ./unique_ptr
CTOR foo
CTOR bar
DTOR bar
DTOR foo
CTOR foo1
Processing foo1
DTOR foo1
CTOR foo2
CTOR foo3
Processing foo2
DTOR foo2
End of main()
DTOR foo3