std::unique_ptr
std::unique_ptr
in C++When we’re writing complicated software that is intended to run for a long time, memory management can get incredibly complex. For example, the server for an online game may be managing hundreds of thousands of objects, in a highly dynamic environment.
Furthermore, these processes may need to run for weeks or months between restarts so even the smallest memory leak will cause issues.
In these environments, we need to be more strategic in how we manage memory - we can’t rely on ad hoc calls to delete
scattered through our code base.
The common way to design this is through a system of ownership. Our objects, and the memory used to store them, are owned by other objects. Those objects are, in turn, owned by further objects. At the top of this hierarchy will be a stack frame - a function.
Designing our applications with the concept of memory ownership greatly simplifies our memory management. This is because when an object gets deleted from memory, all the objects that were owned by it also get deleted. No longer do we need to scatter calls to delete
throughout our code, with all the problems that entails.
The class of objects that implement the memory ownership patterns described above is known as smart pointers.
The simplest form of smart pointer is a unique pointer, an implementation of which is available in the standard library as std::unique_ptr
. Like any pointer, a unique pointer points to an object in memory. The “unique” refers to the idea that it should be the only smart pointer that points to that object.
As such, the function or object that has the unique pointer owns the object that the pointer points at.
Unique pointers implement behaviors to solidify this intent. For example, we’ll see later in this lesson that it is difficult to create a copy of a unique pointer.
make_unique
By including memory
, we gain access to the std::make_unique
function, which is the preferred way of creating unique pointers:
#include <memory>
int main() {
auto Pointer { std::make_unique<int>(42) };
}
The <
and >
within this code indicates that make_unique
is a template function. We cover template functions in detail later in this course. For now, we should just note that we need to pass the type of data we want to create a pointer to within the <
and >
. In this case, that is an int
Within the (
and )
, we can optionally pass any arguments along to the constructor of our data type. In this example, we pass 42
thereby creating a pointer to an int
that has an initial value of 42
.
The return type of make_unique
is a std::unique_ptr
of the corresponding type:
#include <memory>
int main() {
std::unique_ptr<int> Pointer { std::make_unique<int>(42) };
}
By convention, we just use auto
with the return value of make_unique
. This is because the type of the underlying data (eg, int
) is included in the statement already. Additionally, make_unique
is so ubiquitous that C++ developers soon learn that it returns a unique_ptr
so there’s no benefit in constantly repeating that within our code.
Let’s see an example of unique pointers with a class. We’ll use this class throughout the rest of this lesson:
#include <iostream>
#include <memory>
using namespace std;
class Character {
public:
string Name;
Character(string Name = "Frodo") :
Name { Name }
{
cout << "Creating " << Name << endl;
}
~Character() {
cout << "Deleting " << Name << endl;
}
};
int main() {
auto FrodoPointer { make_unique<Character>("Frodo") };
auto GandalfPointer { make_unique<Character>("Gandalf") };
}
This program outputs the following:
Creating Frodo
Creating Gandalf
Deleting Gandalf
Deleting Frodo
As with basic pointers, which are often referred to as “raw” pointers, we can dereference smart pointers and access the object they point at using the *
or ->
 operators:
int main() {
auto FrodoPointer { make_unique<Character>("Frodo") };
cout << "Logging " << (*FrodoPointer).Name << endl << endl;
auto GandalfPointer { make_unique<Character>("Gandalf") };
cout << "Logging " << GandalfPointer->Name << endl << endl;
}
Creating Frodo
Logging Frodo
Creating Gandalf
Logging Gandalf
Deleting Gandalf
Deleting Frodo
Given the design intent of unique pointers, it doesn’t make sense to copy them. As a result, the unique_ptr
class does take some steps to prevent this:
auto FrodoPointer1 { make_unique<Character>() };
auto FrodoPointer2 { FrodoPointer1 };
Line 2 will result in a compilation error, stating the copy constructor has been deleted. We will better understand copy constructors, and add them to our own classes, later in this course.
When working with functions, passing by value is also a form of copying, so this will also be prevented with a similar error message:
void LogName(unique_ptr<Character> Character) {
cout << "Logging " << Character->Name << endl;
}
int main() {
auto FrodoPointer { make_unique<Character>() };
LogName(FrodoPointer);
}
Of course, there are countless situations where we need to pass a pointer to a function. For those scenarios, smart pointers implement the get
function, which returns the underlying raw pointer. This allows other parts of our code to access our objects, without creating copies of our unique pointers:
void LogName(Character* Character) {
cout << "Logging " << Character->Name << endl;
}
int main() {
auto FrodoPointer { make_unique<Character>() };
LogName(FrodoPointer.get());
}
Creating Frodo
Logging Frodo
Deleting Frodo
Sometimes, we don’t just want to give other functions or objects access to our smart pointers - we want them to take ownership of them, too. For those scenarios, we can call the std::move
function available within <utility>
#include <memory>
#include <utility>
void TakeOwnership(std::unique_ptr<int> Number) {
// Do things
}
int main() {
auto Number { std::make_unique<int>() };
TakeOwnership(std::move(Number));
}
Lets see an example with our Character
 class:
void LogName(unique_ptr<Character> Character) {
cout << "Logging " << Character->Name << endl;
}
int main() {
auto FrodoPointer { make_unique<Character>("Frodo") };
LogName(move(FrodoPointer));
auto GandalfPointer { make_unique<Character>("Gandalf") };
LogName(move(GandalfPointer));
}
Note carefully the order of logs here:
Creating Frodo
Logging Frodo
Deleting Frodo
Creating Gandalf
Logging Gandalf
Deleting Gandalf
Previously, when the main
function was maintaining ownership of our smart pointers, our characters were not getting deleted until the end of our program.
Now, because ownership is being transferred to the LogName
function each time it is called, the compiler is dutifully cleaning up our characters every time that function ends.
Attempting to use our pointer after it has been transferred will result in an error:
auto FrodoPointer { make_unique<Character>() };
LogName(move(FrodoPointer));
cout << FrodoPointer->Name;
We can call release()
on a smart pointer. After calling release
, the smart pointer will no longer manage the underlying object. The release will not delete the object, but it will return the raw pointer, which lets us decide what to do with it.
After calling release
, the smart pointer’s get
function will return nullptr
#include <memory>
#include <iostream>
int main() {
using namespace std;
auto SmartPointer { make_unique<Character>() };
Character* RawPointer { SmartPointer.release() };
// This will be a null pointer
cout << "Smart: " << SmartPointer.get() << endl;
cout << "Raw: " << RawPointer << endl;
delete RawPointer;
}
The output is as follows:
Creating Frodo
Smart: 0
Raw: 0x2022e70
Deleting Frodo
The reset
function will delete the managed object. After calling reset
, the get
function will return a nullptr
:
int main() {
auto FrodoPointer { make_unique<Character>() };
FrodoPointer.reset();
cout << FrodoPointer.get() << endl; // nullptr
}
Creating Frodo
Deleting Frodo
0
Typically, reset
is used to update the object that is being managed by a smart pointer. We can do this by passing the new raw pointer as an argument to reset
:
int main() {
auto Pointer { make_unique<Character>("Frodo") };
cout << "Logging " << Pointer->Name << endl;
Pointer.reset(new Character {"Gandalf"});
cout << "Logging " << Pointer->Name << endl;
}
Creating Frodo
Logging Frodo
Creating Gandalf
Deleting Frodo
Logging Gandalf
Deleting Gandalf
Note, when passing an argument to reset
, it’s important to ensure that it is pointing at an object in the free store. The following will cause issues:
int main() {
auto Pointer { make_unique<Character>() };
Character Gandalf;
// Gandalf is allocated on the stack - it is going to
// be deleted when this function ends - storing it in
// a smart pointer does not make sense
Pointer.reset(&Gandalf);
}
Similarly, we should ensure the memory location is not already being managed. Having two unique pointers managing the same area of memory is a recipe for disaster:
int main() {
auto Pointer1 { make_unique<Character>("Frodo") };
auto Pointer2 { make_unique<Character>("Gandalf") };
// We don't want 2 smart pointers managing the same object
// Using release() instead of get() would work here
Pointer1.reset(Pointer2.get());
}
Finally, we have swap
, which accepts another smart pointer as an argument, and swaps the object being managed between the two pointers:
int main() {
auto Pointer1 { make_unique<Character>("Frodo") };
auto Pointer2 { make_unique<Character>("Gandalf") };
cout << "1: " << Pointer1->Name << endl;
cout << "2: " << Pointer2->Name << endl;
Pointer1.swap(Pointer2);
cout << "1: " << Pointer1->Name << endl;
cout << "2: " << Pointer2->Name << endl;
}
Creating Frodo
Creating Gandalf
1: Frodo
2: Gandalf
1: Gandalf
2: Frodo
Deleting Frodo
Deleting Gandalf
const
Unique PointersAs with raw pointers, unique pointers can be const
:
#include <memory>
int main() {
const auto Pointer {
std::make_unique<int>(42)
};
// Error: cannot reset a const pointer
Pointer.reset();
}
const
Similarly, the object that a unique pointer points to can also be const
:
#include <memory>
int main() {
auto Pointer {
std::make_unique<const int>(42)
};
// Error - cannot modify a const int
(*Pointer)++;
}
Both the pointer and the object can be const
:
const auto Pointer {
std::make_unique<const int>(42)
};
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.