Hello, and welcome back. In our last lesson, we explored the Resource Acquisition Is Initialization (RAII) principle, establishing it as the cornerstone of safe resource management in C++. We saw how fragile manual cleanup is and built a custom RAII wrapper, DeviceBuffer, to safely manage a cudaMalloc'd resource. In doing so, we encountered the critical concept of ownership and the dangers of shallow copying, which we solved by deleting the copy operations to enforce a unique ownership policy.
This lesson builds directly on that foundation. Instead of having to write custom RAII wrappers for every resource, the C++ Standard Library provides a set of powerful, general-purpose tools to do this for us. Today, we will focus on the two most important tools for managing heap-allocated memory: std::unique_ptr and std::shared_ptr. These "smart pointers" are lightweight classes that wrap a raw pointer, automating memory deallocation through RAII. By the end of this lesson, you will know how to use them to express clear ownership semantics—either exclusive or shared—and eliminate manual new and delete from your code, making it safer, cleaner, and more expressive.
The Problem with Raw Pointers
Before we dive into the solution, it's worth taking a moment to fully appreciate the problem smart pointers solve. As we've touched on, managing memory with raw pointers (new and delete) is notoriously error-prone. Common bugs include:
- Memory leaks: Forgetting to
deletememory, especially in functions with multiple return paths or in the face of exceptions. - Double frees: Deleting the same memory twice, leading to undefined behavior and likely a crash.
- Dangling pointers: Using a pointer after the memory it pointed to has been deleted.
- Unclear ownership: When looking at a function that takes a raw pointer, it's impossible to know who is responsible for deleting it. Does the function take ownership? Is it just borrowing a reference? This ambiguity is a recipe for bugs.
The following video from a CppCon talk by David Olsen provides a great series of short, compelling examples that highlight these issues.
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
This segment from David Olsen's "Back to Basics: C++ Smart Pointers" clearly illustrates why raw pointers are problematic.
Please watch the introduction, from the beginning up to the section on "What's wrong with T*?". The speaker presents three simple code snippets and challenges the audience to find the bugs, which perfectly demonstrate memory leaks and ownership ambiguity. This sets the stage for why a better system is needed.
As you can see, even simple code can harbor subtle and dangerous memory management bugs. The core issue is that a raw pointer T* is just an address; it carries no information about ownership or lifetime. Smart pointers solve this by encoding ownership semantics into the type system itself.
std::unique_ptr: Exclusive Ownership
The simplest and most common smart pointer is std::unique_ptr. It provides a direct, standardized implementation of the unique ownership model we enforced on our DeviceBuffer class in the previous lesson.
A std::unique_ptr<T> is a lightweight object that exclusively owns a heap-allocated object of type T.
- When the
unique_ptris destroyed (e.g., goes out of scope), its destructor automatically callsdeleteon the raw pointer it holds, freeing the memory. - It is move-only. You cannot copy a
unique_ptr, because that would violate its guarantee of unique ownership. You can, however, move it, which transfers ownership from the source pointer to the destination.
The preferred way to create a unique_ptr is with the helper function std::make_unique. This function allocates memory for the object, constructs the object in that memory, and returns a unique_ptr that owns it.
#include <memory>
struct Widget { /*...*/ };
void process_widget() {
// Create a Widget on the heap, owned by a unique_ptr.
auto my_widget_ptr = std::make_unique<Widget>();
// Use the pointer like a normal pointer.
my_widget_ptr->do_something();
// No need to call delete!
// When my_widget_ptr goes out of scope, the Widget is automatically destroyed.
}
Transferring Ownership
Since unique_ptr is move-only, if you want to transfer ownership, you must do so explicitly using std::move. This makes the transfer of ownership a clear, syntactically distinct operation. Once moved from, the original unique_ptr becomes null and no longer owns the object.
auto ptr1 = std::make_unique<int>(42);
// std::unique_ptr<int> ptr2 = ptr1; // COMPILE ERROR: unique_ptr is not copyable.
// Transfer ownership from ptr1 to ptr2.
std::unique_ptr<int> ptr2 = std::move(ptr1);
// Now, ptr2 owns the integer. ptr1 is null.
// if (ptr1) { /* this block is not entered */ }

The following video provides a more thorough walkthrough of unique_ptr, covering its core features and showing how to use it to refactor raw-pointer code.
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
This part of David Olsen's talk provides a complete introduction to std::unique_ptr.
Please watch the section from the beginning of the unique_ptr segment to its conclusion. Pay close attention to: How unique_ptr automates cleanup (RAII). The concepts of move-only types and transferring ownership. The use of std::make_unique as best practice. How to pass and return unique_ptr from functions to transfer ownership. The "gotchas" at the end, which are important practical advice.
As the video explains, unique_ptr has virtually zero overhead compared to a raw pointer. It's a lightweight abstraction that provides immense safety benefits. Your default choice for managing heap-allocated objects should always be std::unique_ptr.
std::shared_ptr: Shared Ownership
While unique_ptr covers many use cases, sometimes you genuinely need shared ownership. This occurs when an object's lifetime is tied to multiple "owners," and it should only be destroyed when the last owner is finished with it. std::shared_ptr models this behavior.
A std::shared_ptr<T> manages an object of type T through reference counting.
- All
shared_ptrinstances that own the same object point to a shared control block. This control block contains metadata, including a reference count. - When a
shared_ptris copied, the reference count is incremented. - When a
shared_ptris destroyed or reset, the reference count is decremented. - When the reference count drops to zero, it means the last
shared_ptrowning the object is gone. The object is then destroyed and its memory is deallocated.

Similar to unique_ptr, the preferred way to create a shared_ptr is with a helper function, std::make_shared. Using std::make_shared is even more important than make_unique, as it performs a single, more efficient memory allocation for both the object and its control block.
This concept of shared ownership is not just an academic exercise; it's fundamental to the design of many large C++ frameworks, including the PyTorch C++ API that you will be using.
Using the PyTorch C++ Frontend
This section of the official PyTorch tutorial explains exactly why std::shared_ptr is used for managing neural network modules. This provides excellent context for your goals.
Please read the section titled Module Ownership. It explains that to replicate the easy-to-use reference semantics of Python, the C++ frontend uses std::shared_ptr extensively to manage the lifetime of modules like torch::nn::Linear. It also introduces the TORCH_MODULE macro, a clever device used to reduce the boilerplate of using shared_ptr.
As the PyTorch documentation highlights, shared_ptr is essential when an object (like a neural network layer) is logically owned by multiple other objects (e.g., by the main network Net class and also by the underlying torch::nn::Module base class for registration).
The following video segment provides a detailed explanation of shared_ptr's mechanics and usage.
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
This final segment of the "Smart Pointers" talk covers std::shared_ptr in detail.
Please watch from the beginning of the shared_ptr section to its end. Focus on: How the reference counting model works. The importance of using std::make_shared. The critical rule: never create multiple shared_ptrs from the same raw pointer. Always create one, and then copy it. The brief discussion of thread safety, which is relevant for high-performance code.
Choosing the Right Pointer
You now have two powerful tools. The key is to know when to use each. The guideline is simple:
- Default to
std::unique_ptr. It is the simplest, most efficient smart pointer. It clearly expresses exclusive ownership and has no runtime overhead. Most objects should have a single, clear owner. - Use
std::shared_ptronly when you genuinely need shared ownership. This is when an object's lifetime is fundamentally tied to multiple, non-hierarchical owners, and it's not clear which one will be the last to finish. The PyTorch module example is a perfect case study. - Use raw pointers (
T*) for non-owning access. If a function just needs to observe or use an object without affecting its lifetime, pass a raw pointer (obtained viaptr.get()) or, even better, a reference (*ptr).
Here's the final piece of advice from David Olsen's talk, which perfectly summarizes this choice.
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
This short segment provides a clear, actionable rule for deciding between unique_ptr and shared_ptr.
Watch from the section "When to Use". The advice to prefer unique_ptr and only move to shared_ptr when necessary is a core tenet of modern C++ best practices.
Finally, to see both smart pointers used in the PyTorch API, note that while modules use shared_ptr, the function torch::data::make_data_loader returns a std::unique_ptr to a data loader object. This shows that real-world libraries use the right tool for the job, selecting the ownership model that best fits the specific component's role.
Conclusion
In this lesson, we've taken the RAII principle and applied it to memory management using the standard library's smart pointers. You've learned how std::unique_ptr and std::shared_ptr automate memory deallocation, prevent entire classes of common bugs, and make your code's ownership intentions explicit and clear.
Key Takeaways:
- Smart pointers are RAII wrappers for memory: They bind the lifetime of heap-allocated memory to the scope of a pointer object.
std::unique_ptrmodels exclusive ownership: It's lightweight, fast, and move-only. It should be your default choice. Usestd::make_uniqueto create them.std::shared_ptrmodels shared ownership: It uses reference counting to ensure an object is destroyed only when the last owner is gone. It's essential for complex ownership graphs, like those in DL frameworks. Usestd::make_sharedfor efficiency.- Express intent: Use
std::moveto explicitly transfer unique ownership. Useshared_ptrcopies to express a new shared owner. Use raw pointers or references for non-owning access.
With your understanding of RAII and smart pointers, you have now mastered the foundation of modern C++ resource management. In our next lesson, we will move from managing single objects to managing collections of data. We'll explore the core STL containers you'll use constantly in numerical code: std::vector, std::array, and std::span.