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.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

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 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.

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

  • An int object allocated in the free store (dynamic memory)
  • That int having the initial value of 42
  • A 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.

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>

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

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

But shouldn’t we avoid using raw pointers?

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.

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 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

Releasing Unique Pointers

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

Updating Smart Pointers with 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

Pitfall 1: Managing an object that is not in the free store

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

Pitfall 2: Managing an object that is already managed by another smart pointer

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

Swapping Unique Pointers

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 Pointers

Similar to raw pointers, there are two ways we can use const with std::unique_ptr. This creates four possible combinations:

1. Non-const pointer to non-const

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

2. Const pointer to non-const

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

3. Non-const pointer to const

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

4. Const pointer to const

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

Summary

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:

  1. Understanding Memory Ownership: We've learned what smart pointers are and how they implement an ownership model for objects in dynamically allocated memory
  2. Memory Management Simplified: We explored how smart pointers automate memory management, eliminating the need for manual delete calls and thereby reducing the chances of memory-related errors.
  3. Creating Unique Pointers using std::make_unique: We now know how to create unique pointers using std::make_unique,
  4. Access and Ownership Transfer: We learned how to give other functions access to our resources through a raw pointer, generated by get(). We also covered how to transfer ownership of resources using std::move().
  5. Releasing, Resetting and Swapping: We learned about the release(), reset(), and swap() methods, and why we’d use them

Up Next: Shared Pointers

In 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:

  • How shared pointers manage resource sharing.
  • The concept of reference counting in shared pointers.
  • Differences and similarities between unique and shared pointers.
  • Appropriate use cases for shared pointers.
  • Potential pitfalls and how to avoid them.

Was this lesson useful?

Next Lesson

Shared Pointers using std::shared_ptr

An introduction to shared memory ownership using std::shared_ptr
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
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
Next Lesson

Shared Pointers using std::shared_ptr

An introduction to shared memory ownership using std::shared_ptr
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved