Memory + RAII · #5 of 13

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:

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;
}
idle

`make_unique` allocates and returns a `unique_ptr`. When the `unique_ptr` falls out of scope, the destructor calls `delete` for you.

expected output
before scope
+ Widget(alpha)
using alpha
using alpha
- ~Widget(alpha)
after scope
Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out
stack heap main() unique_ptr<Widget> w = Widget { name: "alpha" }
`unique_ptr` is a stack object holding a single owning pointer. Same shape as a raw `Widget*`, with one extra obligation: it deletes on destruction.

The rules unique_ptr enforces at compile time:

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;
}
idle

The ownership-passing factory pattern. The signature alone tells the caller they're getting an owned thing.

expected output
  + 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>.

stack heap main() shared_ptr<T> a = shared_ptr<T> b = control_block { T*, ref=2, weak=0 } T ...
Two `shared_ptr`s pointing at the same control block. The control block holds the reference count plus a pointer to the actual object. Increment on copy, decrement on destruction, delete when count hits zero.

The cost compared to unique_ptr:

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
}
idle

The refcount goes up on copy, down on destruction. The object dies exactly when the last shared_ptr does.

expected output
  + 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:

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:

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

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.