std::unique_ptr
std::unique_ptr
in C++In the world of long-running, complex software like online game servers, managing memory efficiently is critical.
Imagine handling hundreds of thousands of objects without a hitch, for weeks or even months. Here, even a tiny memory leak can snowball into a major issue.
That's where strategic memory management comes in. Instead of littering your code with haphazard delete
calls, we adopt a system of ownership. In this system, objects own other objects, creating a hierarchical structure of responsibility.
This approach not only cleans up our code but also ensures that when one object is deleted, all its dependents are automatically cleaned up too.
Let's dive into how we can simplify and strengthen our memory management with this smart system of ownership.
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 unique pointer that points to that object.
As such, we can imagine that the function or object that owns the unique pointer has exclusive ownership of 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, as doing so would inherently undermine the uniqueness.
make_unique
By including <memory>
, we gain access to the std::make_unique
function. Using this function 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
The net effect of all of this is that we have:
int
object allocated in the free store (dynamic memory)int
having the initial value of 42
std::unique_ptr
, which we’ve called Pointer
, within the stack frame of our main
function. That std::unique_ptr
is considered the sole owner of the integer that is stored in the heap.The return type of make_unique
is a std::unique_ptr
of the corresponding type. For example, std::make_unique<int>
will return a std::unique_ptr<int>
which we can specify if preferred:
#include <memory>
int main() {
std::unique_ptr<int> Pointer {
std::make_unique<int>(42)
};
}
However, when using std::make_unique
, it’s somewhat common to use auto
type deduction.
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 constantly specifying this can add noise to 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>
class Character {
public:
std::string Name;
Character(std::string Name = "Frodo") :
Name { Name }
{
std::cout << "Creating " << Name << '\n';
}
~Character() {
std::cout << "Deleting " << Name << '\n';
}
};
int main() {
auto FrodoPointer{
std::make_unique<Character>("Frodo")
};
auto GandalfPointer{
std::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:
#include <iostream>
#include <memory>
class Character {/*...*/};
int main(){
auto FrodoPointer{
std::make_unique<Character>("Frodo")
};
std::cout << "Logging "
<< (*FrodoPointer).Name << "\n\n";
auto GandalfPointer{
std::make_unique<Character>("Gandalf")
};
std::cout << "Logging "
<< GandalfPointer->Name << "\n\n";
}
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 takes some steps to prevent this. For example, the copy constructor has been deleted, meaning code like this will result in a compilation error:
#include <memory>
int main(){
auto Ptr1{std::make_unique<int>(42)};
auto Ptr2{Ptr1};
}
error: 'std::unique_ptr':
attempting to reference a deleted function
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:
#include <memory>
void SomeFunction(std::unique_ptr<int> Num) {
// ...
}
int main(){
auto Ptr{std::make_unique<int>(42)};
SomeFunction(Ptr);
}
error: 'std::unique_ptr':
attempting to reference a deleted function
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:
#include <memory>
void SomeFunction(int* Num) {
// ...
}
int main() {
auto Ptr{std::make_unique<int>(42)};
SomeFunction(Ptr.get());
}
A common question at this point is based on the fact that we now seem to be using raw pointers again.
Mixing smart pointers and raw pointers in our application is common, and not as problematic as we might think.
We simply establish a convention that any function that uses raw pointers is requesting access to a resource, but is not requesting ownership of it.
As such, these functions should act accordingly. For example, they should not delete
the resource.
For scenarios where we do want to transfer ownership of the resource, we cover ways of doing that in the next section.
For scenarios where we want multiple functions or objects to share ownership of a resource, we introduce std::shared_ptr
later in this chapter.
Sometimes, we don’t just want to give other functions or objects access to our smart pointers - we want them to take ownership of the thing the pointer is managing. For those scenarios, we can call the std::move()
function available within <utility>
#include <memory>
#include <utility>
void TakeOwnership(std::unique_ptr<int> Num) {
// Do things
}
int main() {
auto Number{std::make_unique<int>()};
TakeOwnership(std::move(Number));
}
Let's see an example with our Character
 class:
#include <memory>
#include <utility>
#include <string>
#include <iostream>
class Character {/*...*/};
void LogName(std::unique_ptr<Character> C) {
std::cout << "Logging " << C->Name << '\n';
}
int main() {
auto FrodoPointer{
std::make_unique<Character>("Frodo")};
LogName(std::move(FrodoPointer));
auto GandalfPointer{
std::make_unique<Character>("Gandalf")};
LogName(std::move(GandalfPointer));
}
Note carefully the order of actions from the previous program:
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 the main
 function.
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.
Note that attempting to use a pointer after ownership has been given away can result in unpredictable behavior, because the new owner of the resource may have deleted it.
However, most compilers can detect this, and notify us with a warning:
int main(){
auto FrodoPointer{
std::make_unique<Character>("Frodo")};
LogName(std::move(FrodoPointer));
FrodoPointer->Name;
}
warning: Use of a moved-from object: FrodoPointer
A unique pointer can release ownership over a resource using the release()
 method.
The release()
method will not delete the underlying resource. Instead, 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 a nullptr
:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main() {
auto SmartPointer{
std::make_unique<Character>()
};
Character* RawPointer{
SmartPointer.release()
};
// This will be a null pointer
std::cout << "Smart: "
<< SmartPointer.get() << '\n';
std::cout << "Raw: " << RawPointer << '\n';
delete RawPointer;
}
The output is as follows:
Creating Frodo
Smart: 0
Raw: 0x2022e70
Deleting Frodo
reset()
The reset()
function will also release ownership of the underlying resource, but will also delete it.
After calling reset()
, the get()
function will return a nullptr
:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main() {
auto FrodoPointer{
std::make_unique<Character>()
};
FrodoPointer.reset();
// nullptr
std::cout << FrodoPointer.get() << '\n';
}
Creating Frodo
Deleting Frodo
0
Typically, reset()
is used to update the object that is being managed by the smart pointer. We can do this by passing the new raw pointer as an argument to reset()
:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main() {
auto Pointer{
std::make_unique<Character>("Frodo")};
std::cout << "Logging "
<< Pointer->Name << '\n';
Pointer.reset(new Character{"Gandalf"});
std::cout << "Logging "
<< Pointer->Name << '\n';
}
Creating Frodo
Logging Frodo
Creating Gandalf
Deleting Frodo
Logging Gandalf
Deleting Gandalf
Note, that when passing an argument to reset
, it’s important to ensure that it is pointing at an object in the free store.
The following code is attempting to use a smart pointer that is allocated on the stack, and is therefore going to be automatically deleted.
Having a smart pointer also attempting to manage its deletion will cause issues:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main() {
auto Pointer{std::make_unique<Character>()};
Character Gandalf;
// Gandalf is allocated on the stack - it is
// going to be deleted when this function ends
// Therefore, 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 resource bypasses their intended design, and will cause problems. Below, create a situation where we have multiple unique pointers owning the same resource - the Character
called Gandalf
:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main() {
auto Pointer1{
std::make_unique<Character>("Frodo")};
auto Pointer2{
std::make_unique<Character>("Gandalf")};
// We don't want 2 unique 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:
#include <memory>
#include <string>
#include <iostream>
class Character {/*...*/};
int main(){
auto Pointer1{
std::make_unique<Character>("Frodo")};
auto Pointer2{
std::make_unique<Character>("Gandalf")};
std::cout << "1: " << Pointer1->Name << '\n';
std::cout << "2: " << Pointer2->Name << '\n';
Pointer1.swap(Pointer2);
std::cout << "1: " << Pointer1->Name << '\n';
std::cout << "2: " << Pointer2->Name << '\n';
}
Creating Frodo
Creating Gandalf
1: Frodo
2: Gandalf
1: Gandalf
2: Frodo
Deleting Frodo
Deleting Gandalf
const
Unique PointersSimilar to raw pointers, there are two ways we can use const
with std::unique_ptr
. This creates four possible combinations:
Most of the examples we’ve seen in this lesson have been like this. Neither the pointer nor the object it points to are const
, so we can modify either:
#include <memory>
int main() {
auto Pointer{std::make_unique<int>(42)};
// Modify the underlying object
(*Pointer)++;
// Modify the pointer
Pointer.reset();
}
Here, we cannot modify the pointer, but we can modify the object it points at:
#include <memory>
int main() {
const auto Pointer{
std::make_unique<int>(42)};
// Modify the underlying object
(*Pointer)++;
// Error - can't modify the pointer
Pointer.reset();
}
Here, we can modify the pointer, but not the object it points at:
#include <memory>
int main() {
auto Pointer{
std::make_unique<const int>(42)};
// Error - can't modify the underlying object
(*Pointer)++;
// Modify the pointer
Pointer.reset();
}
Finally, both the pointer and the underlying object can be const
, preventing us from modifying either:
#include <memory>
int main() {
const auto Pointer{
std::make_unique<const int>(42)};
// Error - can't modify the underlying object
(*Pointer)++;
// Error - can't modify the pointer
Pointer.reset();
}
In this lesson, we delved into the concept of unique pointers, a crucial component of modern memory management. Let's briefly recap the key points:
delete
calls and thereby reducing the chances of memory-related errors.std::make_unique
: We now know how to create unique pointers using std::make_unique
,get()
. We also covered how to transfer ownership of resources using std::move()
.release()
, reset()
, and swap()
methods, and why we’d use themIn our next lesson, we'll dive into std::shared_ptr
. This type of smart pointer allows for multiple owners of a single resource, offering a different approach to memory management compared to unique pointers. We'll explore:
std::unique_ptr
An introduction to memory ownership using smart pointers and std::unique_ptr
in C++
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.