Memory Ownership and Smart Pointers

Learn how to manage dynamic memory using unique pointers and the concept of memory ownership

Ryan McCombe
Updated

For the rest of this chapter, we'll focus on memory management. This is a complex topic, so it's normal to struggle with these lessons. Most programming languages do not allow developers to intervene in memory management, so this topic can be difficult even for those already familiar with programming.

In our previous lessons, we've primarily worked with stack memory. Stack memory is straightforward and efficient, automatically managing the lifecycle of our variables. We introduced stack memory briefly earlier in the course:

Dangling Pointers and References

Learn about an important consideration when returning pointers and references from functions

Whilst simple programs can rely entirely on stack memory, it has some limitations that means we need to mix in other techniques in more complex programs. Limitations of stack memory include:

  1. Scope-limited: Variables in stack memory are destroyed when they go out of scope. This prevents us from fully controlling the lifecycle of our objects
  2. Fixed size: Stack memory is allocated at compile-time, so we can't create dynamic data structures that grow or shrink as needed.
  3. Size constraints: The stack size is usually much smaller than the heap, limiting the amount of data we can store.

To overcome these limitations, C++ provides dynamic memory allocation using the heap. We'll dive deeper into dynamic memory in a dedicated chapter in the next course, but for now, let's see a brief overview.

Dynamic Memory and Memory Ownership

Dynamic memory allows us to allocate memory at runtime, giving us more flexibility in managing our program's resources. However, this flexibility comes at a cost: dynamic memory management is error-prone. Common issues include:

  1. Memory leaks: Forgetting to deallocate memory, leading to resource waste.
  2. Dangling pointers: Using pointers that point to already freed memory.
  3. Double deletion: Accidentally freeing the same memory twice.

To mitigate these issues, we can implement the design pattern of memory ownership. This concept suggests that resources (like objects stored in dynamically allocated memory) should have clear owners responsible for their lifecycle. We can imagine that certain objects or functions "own" other resources, managing their creation and destruction.

We saw an example of this in the previous lesson - behind the scenes, the std::vector class assumes ownership of the objects it contains, and manages an area of dynamic memory to store these objects.

Dynamic Arrays using std::vector

Explore the fundamentals of dynamic arrays with an introduction to std::vector

Smart pointers are one way we can implement this ownership model in our own code. In a sense, they try to give us the flexibility of dynamic memory allocation, with the simplicity of stack memory allocation where memory is automatically released when it is no longer needed.

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 holds the unique pointer has exclusive ownership of the object that the pointer points to.

Unique pointers implement restrictions to help enforce this design. 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 mean we now have two unique pointers pointing to the same object, contradicting the uniqueness.

Using std::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 std::make_unique() is a template function. We cover templates in detail in the next 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 the previous example, we wanted to create a pointer to an int, so we pass int between the < and >

Within the ( and ), we can optionally pass any arguments along to the constructor of our data type. In previous example, we pass 42, which will become the initial value of our int.

The net effect of all 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 std::make_unique() is a std::unique_ptr of the corresponding type. For example, std::make_unique<int>() will return a std::unique_ptr<int>:

#include <memory>

int main() {
  std::unique_ptr<int> Pointer {
    std::make_unique<int>(42)
  };
}

When using std::make_unique(), many developers will elect to use auto type deduction:

#include <memory>

int main() {
  auto Pointer{std::make_unique<int>(42)};
}

This is because the type of the underlying data (eg, int) is included in the statement already. Additionally, std::make_unique() is so ubiquitous that C++ developers soon learn that it returns a std::unique_ptr, so repeating this type 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 to 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 directly. The std::unique_ptr class protects against this by preventing its objects from being copied.

We will cover how to implement this capability for our own classes later, thereby controlling (or preventing) the process whereby objects of our custom types are copied.

We can see this in action by trying to copy a std::unique_ptr:

#include <memory>

int main(){
  auto Ptr1{std::make_unique<int>(42)};
  auto Ptr2{Ptr1};  
}
error: 'std::unique_ptr': attempting to reference a deleted function

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

Within the memory ownership paradigm, we can imagine that any function that accepts a raw pointer to a resource is simply requesting access to that resource, but not requesting ownership. The resource is owned by whoever has the std::unique_ptr to it.

For scenarios where we want to transfer ownership of the resource, we can use std::move().

Transferring Ownership using std::move()

Sometimes, we want to transfer ownership of a resource from one unique pointer to another.

This is where std::move() comes in handy. It's a function available in the <utility> header that allows us to transfer ownership of a unique pointer.

This mechanism allows us to transfer ownership of resources between different parts of our program, ensuring that at any given time, there's only one owner of the resource.

#include <memory>
#include <utility>
#include <iostream>

void TakeOwnership(std::unique_ptr<int> Num) {
  std::cout << "TakeOwnership function now "
    "owns the pointer.\n";
  std::cout << "Value: " << *Num << '\n';
}

int main() {
  auto Number{std::make_unique<int>(42)};
  std::cout << "main function owns the pointer.\n";

  TakeOwnership(std::move(Number)); 

  // Number is now in a "moved-from" state
  if (Number == nullptr) {
    std::cout << "Number no longer owns any object.";
  }
}
main function owns the pointer.
TakeOwnership function now owns the pointer.
Value: 42
Number no longer owns any object.

In this example:

  1. We create a std::unique_ptr called Number in the main function.
  2. We use std::move() to transfer ownership of the integer to the TakeOwnership() function.
  3. After the move, Number in main no longer owns any object (it becomes a null pointer).

It's important to note that after using std::move(), the original pointer (Number in this case) is left in a valid but unspecified state. It's safe to reassign it or let it go out of scope, but you shouldn't try to use the object it previously owned.

#include <memory>
#include <utility>
#include <iostream>

void TakeOwnership(std::unique_ptr<int> Num) {}

int main() {
  auto Number{std::make_unique<int>(42)};
  TakeOwnership(std::move(Number)); 
  std::cout << *Number; 
}

Most compilers can detect this, and will generate a warning:

Warning: Use of a moved from object: 'Number'

Summary

In this lesson, we explored 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, 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().
Next Lesson
Lesson 46 of 60

Copy Constructors and Operators

Explore advanced techniques for managing object copying and resource allocation

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Performance of Unique Pointers
What's the performance overhead of using unique pointers compared to raw pointers in C++?
Unique Pointers to Const Objects
Is it possible to create a unique pointer to a const object in C++?
Deleting Raw Pointers from Unique Pointers
What happens if I try to delete the raw pointer obtained from the get() method of a unique pointer?
Thread Safety of Unique Pointers
Is it safe to use unique pointers in multithreaded applications?
Reset vs Release for Unique Pointers
What's the difference between reset() and release() for unique pointers?
Unique Pointers with C-style APIs
Can I use unique pointers with C-style APIs that expect raw pointers?
Returning Unique Pointers from Functions
What's the best way to return a unique pointer from a function?
Copyable Classes with Unique Pointers
How can I use unique pointers in a class that needs to be copyable?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant