In our last lesson, you did the heavy lifting of manually implementing the Rule of Five for our DynamicArray class. You now have a deep, first-principles understanding of how to manage resource ownership, copying, and moving. While this is a foundational skill, you probably also noticed it's verbose and error-prone. What if the standard library could handle all that boilerplate for you?
This lesson introduces C++ smart pointers, the modern solution to manual memory management. They are wrapper classes around raw pointers that automate resource cleanup by leveraging the RAII (Resource Acquisition Is Initialization) principle you've been practicing. We'll explore std::unique_ptr for exclusive ownership and std::shared_ptr for shared ownership. By the end, you'll be able to prevent memory leaks and write cleaner, safer, and more expressive C++ code, moving you closer to the "Rule of Zero"—where your classes often need no custom resource management logic at all.
1. The Problem with Raw Pointers
Before diving into the solution, let's briefly crystallize the problem. Manually managing new and delete is a primary source of bugs in C++.
The mCoding channel provides an excellent demonstration of just how easily things can go wrong with raw pointers, leading to memory leaks or double-deletions.
unique_ptr: C++'s simplest smart pointer
Watch this video segment from "unique_ptr: C++'s simplest smart pointer".
First, watch the introduction here, which frames smart pointers as a solution to common bugs. Then, pay close attention to the practical example from this segment. The presenter walks through a typical use case with a std::vector of raw pointers and highlights multiple failure points, like forgetting to call delete or accidentally deleting a pointer twice. This perfectly illustrates the issues our DynamicArray from the last two lessons was designed to solve.
Smart pointers abstract away these dangers by enforcing clear ownership rules at the language level.
2. std::unique_ptr: Exclusive Ownership
The simplest and most common smart pointer is std::unique_ptr. It enforces exclusive ownership: only one unique_ptr can own a dynamically allocated object at any given time. When the unique_ptr is destroyed (for example, when it goes out of scope), its destructor automatically calls delete on the raw pointer it manages.
Key characteristics of std::unique_ptr:
- Lightweight: It has virtually no performance or memory overhead compared to a raw pointer. It's just a thin wrapper.
- Non-copyable: Its copy constructor and copy assignment operator are deleted. This is by design. If you could copy a
unique_ptr, you'd have two owners, and both would try todeletethe same memory, leading to the double-free error you're now very familiar with. - Movable: Ownership can be transferred from one
unique_ptrto another usingstd::move. This is the only way to "pass" a unique pointer. The source pointer is left in a null state.
Creating and Using std::unique_ptr
The preferred way to create a unique_ptr is with the std::make_unique helper function, which was introduced in C++14.
#include <iostream>
#include <memory>
class Widget {
public:
Widget() { std::cout << "Widget created\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
void do_something() { std::cout << "Doing something...\n"; }
};
int main() {
{ // Inner scope
// Create a Widget on the heap, managed by a unique_ptr
std::unique_ptr<Widget> widget_ptr = std::make_unique<Widget>();
// Use the pointer just like a raw pointer
widget_ptr->do_something();
} // widget_ptr goes out of scope here. Its destructor is called,
// which in turn calls delete on the Widget object.
std::cout << "Exited inner scope.\n";
return 0;
}
Output:
Widget created
Doing something...
Widget destroyed
Exited inner scope.
Notice we never called delete. The unique_ptr handled it for us automatically and safely.
The following video walks through building a simplified unique_ptr from scratch. Given your goal of understanding systems from first principles, this will give you a clear mental model of how it works.
unique_ptr: C++'s simplest smart pointer
Watch these sections from the "unique_ptr" video by mCoding.
First, see the transition to using std::unique_ptr here. Note the use of std::make_unique and std::move. Next, follow the from-scratch implementation of MyUniquePtr in this segment. This covers the core components: a member pointer, a destructor that calls delete, deleted copy operations, and the crucial move constructor/assignment. Finally, observe the implementation of the convenience operators (->, *, bool) that make it feel like a raw pointer here.
3. std::shared_ptr: Shared Ownership
What if you need multiple owners? For instance, several objects in a data structure might need to refer to and share the lifetime of a single resource. This is where std::shared_ptr comes in.
std::shared_ptr uses a technique called reference counting to manage shared ownership.
- Internally, a
shared_ptrpoints to both the managed object and a separate control block. - This control block stores, among other things, a "strong reference count."
- When a
shared_ptris copied, the reference count is incremented. - When a
shared_ptris destroyed or reset, the reference count is decremented. - The managed object is deleted only when the reference count drops to zero.

Creating and Using std::shared_ptr
Similar to unique_ptr, std::shared_ptr has a helper function, std::make_shared. Using make_shared is even more important for shared_ptr because it can perform a single, more efficient memory allocation for both the object and its control block.
#include <iostream>
#include <memory>
// Using the same Widget class from before
void observe(std::shared_ptr<Widget> ptr) {
std::cout << "Observing. Ref count: " << ptr.use_count() << "\n";
}
int main() {
std::shared_ptr<Widget> p1; // Starts as null
{ // Inner scope
auto p2 = std::make_shared<Widget>();
std::cout << "p2 created. Ref count: " << p2.use_count() << "\n"; // Count is 1
p1 = p2; // Copy assignment. Both now own the Widget.
std::cout << "p1 owns it too. Ref count: " << p1.use_count() << "\n"; // Count is 2
observe(p1); // Pass by value creates another temporary copy
std::cout << "Back in main. Ref count: " << p1.use_count() << "\n"; // Count is 2 again
} // p2 goes out of scope. Destructor runs, ref count drops to 1.
// The Widget is NOT destroyed.
std::cout << "Exited inner scope. Ref count: " << p1.use_count() << "\n"; // Count is 1
// p1 goes out of scope at the end of main. Ref count drops to 0.
// The Widget is finally destroyed.
return 0;
}
Output:
Widget created
p2 created. Ref count: 1
p1 owns it too. Ref count: 2
Observing. Ref count: 3
Back in main. Ref count: 2
Exited inner scope. Ref count: 1
Widget destroyed
The Cherno gives a great, quick walkthrough of both unique_ptr and shared_ptr in this video.
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
Watch this video segment from "SMART POINTERS in C++" by The Cherno.
The section on unique_ptr from the beginning serves as a great recap. Pay close attention to the shared_ptr explanation from this timestamp. He clearly explains reference counting and demonstrates why make_shared is preferred for performance.
4. Breaking Cycles with std::weak_ptr
Reference counting is powerful, but it has one major weakness: circular references. If two objects hold shared_ptrs to each other, they create a cycle. Their reference counts will never drop to zero, and their memory will be leaked, even if no external code holds a pointer to them.
struct Child;
struct Parent {
~Parent() { std::cout << "Parent destroyed\n"; }
std::shared_ptr<Child> child;
};
struct Child {
~Child() { std::cout << "Child destroyed\n"; }
std::shared_ptr<Parent> parent; // This creates a cycle!
};
// ... in main()
auto p = std::make_shared<Parent>();
auto c = std::make_shared<Child>();
p->child = c;
c->parent = p;
// When p and c go out of scope, their ref counts are both 1.
// p is kept alive by c, and c is kept alive by p. Memory leak!
The solution is std::weak_ptr. A weak_ptr is a non-owning smart pointer. It can observe an object managed by a shared_ptr but does not increase the reference count.
To safely use a weak_ptr, you must lock() it. This attempts to create a new shared_ptr from the weak_ptr.
- If the original object is still alive,
lock()returns a validshared_ptr. - If the original object has been destroyed,
lock()returns a nullshared_ptr.
By making one of the links in a cycle a weak_ptr (e.g., the child's pointer to the parent), you break the cycle and prevent the memory leak.
Understanding Memory Management, Part 3: C++ Smart ...
This article provides an in-depth explanation of shared_ptr and the circular reference problem.
First, read the section Shared Pointers for a detailed look at how shared_ptr works internally. Next, focus on the section Circular References. The Parent/Child example clearly demonstrates how a memory leak occurs. Finally, read the section on Weak Pointers, which introduces the solution and explains how weak_ptr::lock() works.
5. Project: The Rule of Zero
Now, let's apply this knowledge. In the last lesson, you implemented the Rule of Five for DynamicArray. In this project, you'll create a new class that achieves the same safety and efficiency with almost no boilerplate code, demonstrating the Rule of Zero.
The Rule of Zero states that classes that have custom destructors, copy/move constructors, or copy/move assignment operators should deal exclusively with ownership. Conversely, classes that do not deal with ownership should not need any of these special member functions. Smart pointers allow us to follow this rule.
Your Task:
-
Create a new header file,
SafeArray.h. -
Inside, define a template class
SafeArray<T>. -
Instead of a raw
T* m_Data, use a smart pointer to manage the array:std::unique_ptr<T[]> m_Data;Note the
T[]syntax, which tells theunique_ptrto calldelete[]instead ofdeletewhen it's destroyed. -
Implement the constructor,
push_back, and any other methods you had inDynamicArray. The key difference will be in how you handle reallocation. Whenpush_backneeds to grow the array, you will:
a. Create a newstd::unique_ptr<T[]>with the larger capacity.
b. Copy or move the elements from the old buffer to the new one.
c. Usestd::moveto transfer ownership of the new buffer tom_Data. The oldunique_ptrwill be destroyed, automatically freeing the old memory.// Inside your grow() or realloc() function std::unique_ptr<T[]> new_data = std::make_unique<T[]>(new_capacity); // ... copy/move elements from m_Data to new_data ... m_Data = std::move(new_data); // Ownership transferred, old memory is freed. -
Crucially, do not implement any of the Rule of Five functions. No custom destructor, no copy/move constructors or assignment operators.
-
In
main.cpp, test that yourSafeArraycan be moved but not copied.SafeArray<int> A; A.push_back(10); SafeArray<int> B = std::move(A); // This should compile (move construction) // SafeArray<int> C = B; // This should NOT compile (copy construction is deleted)
You will discover that because std::unique_ptr is movable but not copyable, your SafeArray class inherits this behavior automatically. You've created a safe, resource-managing class without writing a single line of custom ownership logic. This is the power of smart pointers.

Conclusion
You have now mastered the modern C++ approach to memory management. You've seen how std::unique_ptr provides efficient, compile-time-enforced exclusive ownership, and how std::shared_ptr provides flexible shared ownership via reference counting.
Key Takeaways:
- Smart pointers automate memory management using the RAII idiom, preventing leaks and double-frees.
std::unique_ptris for exclusive ownership. It's lightweight and should be your default choice. Ownership is transferred withstd::move.std::shared_ptris for shared ownership. It uses reference counting to decide when to free the resource. Use it only when shared ownership is a clear requirement.std::weak_ptris a non-owning observer forshared_ptrthat breaks circular references.- The Rule of Zero is a design principle that encourages creating simple classes that delegate resource ownership to dedicated classes like smart pointers.
In our next lesson, we'll shift gears slightly. Having solidified your understanding of memory management and class design, we'll dive into another cornerstone of generic, high-performance C++: templates. You'll learn how to write template functions and classes, which will be essential for building the generic, reusable components of our future inference engine.