Hello! Welcome to the third lesson in our C++ foundations module.
In our last lesson, we explored std::vector as a way to manage dynamic collections. We ended with a preview of the hittable_list class from Ray Tracing in One Weekend, which used std::vector<shared_ptr<hittable>>. This raised an important question: what is std::shared_ptr, and why is it used here instead of storing objects or raw pointers directly?
Today, we'll answer that question. This lesson addresses the learning outcome: use smart pointers (std::unique_ptr, std::shared_ptr) to manage the lifetime of heap-allocated objects. You will learn how modern C++ automates memory management, a crucial skill for building a robust and stable ray tracer. This will free you from the error-prone task of manually calling new and delete, which is a common source of bugs like memory leaks and crashes.
1. The Problem with Manual Memory Management
In C++, you can allocate memory on the stack or on the heap. Stack memory is automatic—when a variable goes out of scope, its memory is automatically reclaimed. Heap memory, allocated with new, is not. You are responsible for freeing it with delete. Forgetting to do so causes a memory leak. Deleting it more than once causes undefined behavior, which often leads to a crash.
Managing new and delete manually becomes very difficult in complex applications where pointers to objects are passed around. Who is responsible for deleting the object? When is it safe to do so? Smart pointers solve this problem by wrapping a raw pointer and using a C++ feature called RAII (Resource Acquisition Is Initialization). The core idea is simple: the smart pointer "owns" the object, and when the smart pointer itself is destroyed (e.g., when it goes out of scope), its destructor automatically calls delete on the raw pointer it manages.
To see why this is so important, let's watch a quick introduction.
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
This video from The Cherno provides a great, high-level introduction to what smart pointers are and the problem they solve.
Watch from 00:30 to 01:13. Focus on the core idea: smart pointers automate the process of calling delete for memory allocated with new.
There are two main types of smart pointers we'll focus on, each representing a different kind of "ownership."
2. std::unique_ptr: Exclusive Ownership
The simplest smart pointer is std::unique_ptr. It enforces a strict, exclusive ownership model: only one unique_ptr can own a given object at any time. It's incredibly lightweight, having no performance or memory overhead compared to a raw pointer.

Because of this strict ownership, you cannot copy a std::unique_ptr. If you could, you'd have two unique_ptrs that both think they own the object, and both would try to delete it upon destruction.
However, you can move a unique_ptr, which transfers ownership from the source to the destination. After the move, the source unique_ptr becomes empty (null).
To use smart pointers, you need to include the <memory> header.
#include <memory>
#include "MyObject.h" // Assume we have a class MyObject
// The preferred way to create a unique_ptr
std::unique_ptr<MyObject> ptr1 = std::make_unique<MyObject>();
// ptr2 now owns the object. ptr1 is now null.
std::unique_ptr<MyObject> ptr2 = std::move(ptr1);
// This will cause a compile error:
// std::unique_ptr<MyObject> ptr3 = ptr2; // Error: unique_ptr cannot be copied.
// When ptr2 goes out of scope, the MyObject it owns is automatically deleted.
Let's see unique_ptr in action.
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
The same video from The Cherno now demonstrates the behavior of std::unique_ptr.
Watch from 01:13 to 04:52. Pay close attention to: How the object is automatically destroyed when the unique_ptr goes out of scope. The compile error that occurs when you try to copy a unique_ptr. The use of std::make_unique as the preferred way to create one.
3. std::shared_ptr: Shared Ownership
Exclusive ownership is great, but sometimes you genuinely need multiple parts of your application to co-own an object. For instance, in a scene graph, several nodes might reference the same material or texture. If any one of them were the exclusive owner, the resource might be deleted while others still need it.
This is where std::shared_ptr comes in. It allows for shared ownership of a resource. It works by using a technique called reference counting.
A small, hidden data structure called a "control block" is allocated alongside the object. This control block contains a counter that tracks how many shared_ptrs are currently pointing to the object.
- When you create a new
shared_ptrby copying an existing one, the reference count is incremented. - When a
shared_ptris destroyed (or reset), the count is decremented. - When the count reaches zero, it means no
shared_ptrs are left pointing to the object, and it is safely deleted.

The syntax is very similar to unique_ptr. The preferred way to create a shared_ptr is with std::make_shared. This is more efficient than creating the object with new and then passing it to a shared_ptr constructor, because make_shared can allocate the memory for both the object and its control block in a single operation.
#include <memory>
#include "MyObject.h"
// Create a shared_ptr. Ref count is now 1.
std::shared_ptr<MyObject> s_ptr1 = std::make_shared<MyObject>();
{
// Create a copy. Ref count is now 2.
std::shared_ptr<MyObject> s_ptr2 = s_ptr1;
std::cout << "Ref count inside scope: " << s_ptr1.use_count() << std::endl; // Prints 2
} // s_ptr2 goes out of scope, ref count becomes 1.
std::cout << "Ref count outside scope: " << s_ptr1.use_count() << std::endl; // Prints 1
// When s_ptr1 goes out of scope, the ref count becomes 0, and the object is deleted.
Let's watch the final segment of the video to see this in practice.
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
This part of the video explains std::shared_ptr and reference counting.
Watch from 04:52 to 08:17. Note the differences from unique_ptr: The ability to copy shared_ptr freely. How the object is only destroyed when the last shared_ptr owning it is destroyed. The efficiency benefit of using std::make_shared.
Test your understanding!
Consider the following code snippet:
#include <memory>
void take_ownership(std::unique_ptr<int> p) {
// ... does something with p
}
void share_ownership(std::shared_ptr<int> p) {
// ... does something with p
}
int main() {
auto u_ptr = std::make_unique<int>(10);
auto s_ptr = std::make_shared<int>(20);
// Which of the following lines will compile, and which will fail? Why?
// Line 1
take_ownership(u_ptr);
// Line 2
share_ownership(s_ptr);
// Line 3
if (u_ptr) { /* do something */ }
return 0;
}
Show answer
- Line 1
take_ownership(u_ptr);: This will fail to compile.u_ptris astd::unique_ptr, which cannot be copied. To pass it to the function, you must explicitly transfer ownership usingstd::move:take_ownership(std::move(u_ptr));. - Line 2
share_ownership(s_ptr);: This will compile successfully.s_ptris astd::shared_ptr, which can be copied. Whens_ptris passed to the function, a copy is made, and the reference count increases to 2. - Line 3
if (u_ptr): After a successful move (e.g.,std::move(u_ptr)), this condition would be false, becauseu_ptrwould be null after its managed object was moved away.
4. Smart Pointers in Our Ray Tracer
Now, let's connect this back to std::vector<shared_ptr<hittable>>. To understand the design choice, we can look at the Ray Tracing in One Weekend text.
Ray Tracing in One Weekend Series
This reading from the course textbook explains why shared_ptr is used to build a list of scene objects.
Please read the section titled 'A List of Hittable Objects', focusing on the subsection 'Some New C++ Features'. This will solidify your understanding of why shared_ptr is a good choice for our scene's object list.
As the text explains, using std::shared_ptr in our hittable_list gives us two major benefits:
- Polymorphism: The vector can hold pointers to any object that inherits from
hittable(spheres, triangles, etc.). This allows us to build a flexible scene with different types of geometry. - Automatic Memory Management: When our
hittable_listis destroyed, its vector ofshared_ptrs is also destroyed. This automatically handles decrementing the reference counts and deleting any objects that are no longer referenced, preventing memory leaks without any manual cleanup code.
Furthermore, shared_ptr is essential for another reason that will become important soon: sharing materials. In a complex scene, you might have hundreds of objects that all use the same material (e.g., "red plastic"). Instead of creating a separate material object for each one, we can create a single material instance and have all the relevant objects point to it using a shared_ptr. This is efficient and allows for easy updates.
The text also shows this in practice, by adding a material pointer to the sphere class.
Ray Tracing in One Weekend Series
This second reading shows the practical integration of shared_ptr for managing materials within our geometric objects.
Please read the text and code under the headings 'A Data Structure to Describe Ray-Object Intersections' and the sphere class that follows. Notice how both hit_record and sphere are updated to include a shared_ptr<material>. This is a perfect example of shared ownership in action.
5. unique_ptr vs. shared_ptr: Which Should I Use?
With two types of smart pointers, it's natural to ask which one is better. Here's the standard C++ guideline:
- Default to
std::unique_ptr. It perfectly represents the common case of a single owner. It's simple, fast, and has no overhead. It clearly communicates your design intent: this pointer is the sole owner of this resource. - Use
std::shared_ptronly when you explicitly need shared ownership. If you can't identify a single owner for an object's lifetime, thenshared_ptris the correct tool. The reference counting has a small performance cost, so you should only pay it when necessary.
A brief note on an advanced topic: shared_ptr can lead to circular references, where two objects hold shared_ptrs to each other. This prevents the reference count from ever reaching zero, causing a memory leak. C++ provides std::weak_ptr to break these cycles, but that is a more advanced tool for specific situations.
Key Takeaways
- Smart pointers are C++ objects that wrap raw pointers to automate memory management, preventing leaks and other bugs.
- They work on the principle of RAII: resource lifetime is tied to the scope of the smart pointer object.
std::unique_ptrprovides exclusive ownership. It is lightweight but cannot be copied, only moved. This should be your default choice.std::shared_ptrprovides shared ownership via reference counting. It can be copied and is used when multiple owners need to manage an object's lifetime.- Always prefer
std::make_uniqueandstd::make_sharedto create smart pointers.
Next Steps
You've now covered the core C++ tools for managing objects and collections: std::vector and smart pointers. We have one final foundational lesson before we can start rendering our first image. In the next lesson, you will learn how to write image data to files using std::ofstream. This will allow us to save the output of our ray tracer as an image file that we can view.