Memory Ownership with Smart Pointers, and std::unique_ptr

An introduction to memory ownership using smart pointers and std::unique_ptr in C++
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

dswolf.jpg
Ryan McCombe
Ryan McCombe
Posted

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.

Smart Pointers and Unique Pointers

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.

Creating Unique Pointers with 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.

Dereferencing Unique Pointers

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

Copying Unique Pointers

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

Transferring Ownership of Unique Pointers

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;

Releasing Smart Pointers

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

Updating Smart Pointers with Reset

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());
}

Swapping Smart Pointers

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 Pointers

As 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();
}

Unique Pointers to 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)
};

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Memory Management
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Shared Pointers using shared_ptr

An introduction to shared memory ownership using std::shared_ptr
vb2.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved