RAII + Smart Pointers
How to use the heap without remembering to free it — by making the language do it for you
Why it matters
Lesson 03 ended on the smell: every new needs a matching delete, and
forgetting either is a leak or a crash. Lesson 04 said the right answer
isn’t to be more careful — it’s to push the responsibility into a type.
That type already exists. Two of them, actually:
std::unique_ptr<T>— sole owner. When it dies, it deletes.std::shared_ptr<T>— refcounted shared ownership. The last reference to die runs the destructor.
Together they reduce raw new and delete in modern code to a vanishing
edge case. Once you internalize them, the entire class of memory bugs that
makes C++ scary mostly stops happening.
RAII, in one sentence
Resource Acquisition Is Initialization. Or, plainly: tie a resource’s
lifetime to a stack variable’s lifetime, then let scope-exit do the
cleanup. Same idea as Python’s with, except baked into the destructor of
every type that owns anything.
The pattern, applied to memory:
An object grabs the heap in its constructor. It releases the heap in its destructor. Because destructors fire automatically when the owning object goes out of scope, the heap is freed automatically too.
That’s std::unique_ptr in one paragraph.
unique_ptr: one owner, no overhead
A std::unique_ptr<T> is a thin wrapper around a T* plus a destructor
that calls delete. It’s the same size as a raw pointer (8 bytes on a
64-bit system). It has zero runtime overhead compared to new + manual
delete — the optimizer sees right through it.
#include <iostream>
#include <memory>
#include <string>
struct Widget {
std::string name;
Widget(std::string n) : name(std::move(n)) {
std::cout << " + Widget(" << name << ")\n";
}
~Widget() {
std::cout << " - ~Widget(" << name << ")\n";
}
};
void use(const Widget& w) {
std::cout << " using " << w.name << "\n";
}
int main() {
std::cout << "before scope\n";
{
auto w = std::make_unique<Widget>("alpha"); // heap-allocate, owned by w
use(*w); // * to access the Widget
use(*w);
// no delete needed
}
std::cout << "after scope\n";
return 0;
}
`make_unique` allocates and returns a `unique_ptr`. When the `unique_ptr` falls out of scope, the destructor calls `delete` for you.
before scope
+ Widget(alpha)
using alpha
using alpha
- ~Widget(alpha)
after scope Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out The rules unique_ptr enforces at compile time:
- Move-only. You cannot copy a
unique_ptr. The compiler refuses. Otherwise you’d have two owners, both of which would try todelete. - Move transfers ownership.
std::unique_ptr<T> b = std::move(a);leavesaholdingnullptrandbowning the heap object. - Destruction releases. When the
unique_ptrdies (scope exit, vector shrink, container destructor, whatever),deleteruns exactly once.
Use unique_ptr whenever a single function or object should own a heap
allocation. That covers >95% of modern dynamic allocation.
make_unique is the spelling that matters
auto p = std::make_unique<Widget>(arg1, arg2); is the canonical way to
create one. Skip new even here:
// Don't write this:
std::unique_ptr<Widget> bad(new Widget(args));
// Write this:
auto good = std::make_unique<Widget>(args);
make_unique is two-line shorter, never leaks if the constructor throws
mid-expression, and reads as “I want a unique-owned heap Widget” rather
than “I want a Widget via new, wrapped in a unique_ptr.”
Returning a unique_ptr is how ownership crosses a boundary
This is the modern replacement for “the function calls new and the
caller is responsible for delete.” The compiler now enforces the
contract:
#include <iostream>
#include <memory>
#include <string>
struct Widget {
std::string name;
Widget(std::string n) : name(std::move(n)) { std::cout << " + " << name << "\n"; }
~Widget() { std::cout << " - " << name << "\n"; }
};
// Factory: owns the Widget while it builds it, then hands ownership out.
std::unique_ptr<Widget> make_widget(std::string n) {
return std::make_unique<Widget>(std::move(n));
}
// Consumer: takes ownership, will delete when scope ends.
void consume(std::unique_ptr<Widget> w) {
std::cout << " consuming " << w->name << "\n";
} // ← w dies here
int main() {
auto w = make_widget("alpha"); // ownership returned to main
consume(std::move(w)); // ownership moved into consume()
std::cout << "after consume; w is " << (w ? "alive" : "empty") << "\n";
return 0;
}
The ownership-passing factory pattern. The signature alone tells the caller they're getting an owned thing.
+ alpha
consuming alpha
- alpha
after consume; w is empty Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out std::move(w) doesn’t do anything — it tells the compiler “treat w as
a temporary so move-construction is allowed.” After the move, w holds
nullptr (the moved-from state of unique_ptr). Lesson 06 is all about
this mechanism in detail.
shared_ptr: shared ownership, refcounted
Sometimes one object isn’t enough — the same heap allocation needs to be
owned by multiple holders, and the last one out turns off the lights.
That’s std::shared_ptr<T>.
The cost compared to unique_ptr:
- 16 bytes on the stack (pointer + control-block pointer), not 8.
- Atomic refcount increment/decrement on every copy and destruction. Cheap individually, but in a tight loop, measurable.
- One extra heap allocation for the control block, unless you use
make_sharedwhich fuses it with the object’s allocation.
The rule: default to unique_ptr. Reach for shared_ptr only when
genuinely no single owner exists — e.g. a graph node that several other
nodes point at, with no natural root.
#include <iostream>
#include <memory>
struct Note {
int n;
Note(int v) : n(v) { std::cout << " + Note(" << n << ")\n"; }
~Note() { std::cout << " - ~Note(" << n << ")\n"; }
};
int main() {
auto a = std::make_shared<Note>(42); // refcount = 1
std::cout << "after a: use_count = " << a.use_count() << "\n";
{
auto b = a; // refcount = 2
std::cout << "after b: use_count = " << a.use_count() << "\n";
} // b dies → refcount = 1
std::cout << "after }: use_count = " << a.use_count() << "\n";
return 0; // a dies → refcount = 0 → ~Note
}
The refcount goes up on copy, down on destruction. The object dies exactly when the last shared_ptr does.
+ Note(42)
after a: use_count = 1
after b: use_count = 2
after }: use_count = 1
- ~Note(42) Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out weak_ptr: the non-owning observer
shared_ptr has one trap: cycles leak. If A’s shared_ptr<B> points
at a B whose shared_ptr<A> points back, neither refcount ever reaches
zero. Both objects live forever.
std::weak_ptr<T> is the answer. A weak_ptr doesn’t bump the refcount.
To use the object it points at, you call .lock(), which either gives you
a fresh shared_ptr (if the object is still alive) or a null one (if it
was destroyed).
Use weak_ptr for “back pointers” in any structure where ownership has a
direction but observers can look both ways:
struct Parent { std::vector<std::shared_ptr<Child>> kids; };
struct Child { std::weak_ptr<Parent> parent; }; // breaks the cycle
weak_ptr is rare in code that doesn’t need it. When you do need it, you
really need it.
When raw new/delete is still acceptable
Almost never, in application code. The remaining legitimate cases:
- Implementing a smart-pointer type. Someone has to call
new. That someone should be the standard library, or you, exactly once, in a destructor-paired-with-constructor pattern. - Interfacing with a C API that expects a raw pointer and explicit
destruction. Wrap it in a
unique_ptrwith a custom deleter the moment it crosses into C++ territory. - Placement new for custom allocators (lesson 10’s capstone topic).
If you’re writing application code and reach for new, you’re probably
either constructing something that should be on the stack or constructing
something that should be in a make_unique call. There is almost always
a better spelling.
RAII generalizes — it’s not just for memory
The pattern is broader than smart pointers. Anything you can acquire and release benefits from a wrapper type whose destructor handles the release:
std::lock_guard<std::mutex>— acquires the lock in the constructor, releases it in the destructor. Forgetting to unlock becomes impossible.std::fstream— opens the file in the constructor, flushes and closes in the destructor.std::scoped_lock— same, but for multiple mutexes at once with deadlock-avoidance.std::jthread— joins (or detaches) on destruction. Threads stop leaking.
This is why C++ programmers tend to write small classes whose entire purpose is to own one resource. It’s not architectural maximalism — it’s the language giving you a hook to make resource cleanup automatic.
Key takeaways
- Prefer
std::unique_ptr<T>. Same size asT*, same speed, plus the destructor doesdeletefor you. Usestd::make_uniqueto construct. - Use
std::shared_ptr<T>only when ownership is genuinely shared. Two pointers, atomic refcount, control-block overhead. Worth it when you need it; wasteful otherwise. - Cycles in
shared_ptrgraphs leak. Useweak_ptrfor back-pointers or “may have died” observers. newanddeleteshould be rare in modern C++ application code. Containers and smart pointers eat ~99% of the use cases.- RAII is bigger than memory. Every resource (locks, files, sockets, threads) has a wrapper whose destructor releases it. Build one whenever you can.
What’s next
Lesson 06 unpacks move semantics — the language mechanism that lets
unique_ptr transfer ownership without copying, and lets vector
double in size without copying every element. The Rule of Zero, the Rule
of Five, and why most of the time you should write neither.