3. Smart Pointers¶
What & why¶
When you do need a heap-allocated object, a smart pointer owns it and deletes it automatically — RAII for single objects. More importantly, the type you choose documents ownership: who is responsible for the object's lifetime. That's the question behind most C++ memory bugs, and smart pointers make the answer explicit.
All three live in <memory>.
std::unique_ptr: exactly one owner¶
#include <memory>
struct Widget {
int id;
explicit Widget(int i) : id{i} {}
};
int main() {
std::unique_ptr<Widget> w = std::make_unique<Widget>(7);
std::cout << w->id << '\n'; // use like a pointer
std::cout << (*w).id << '\n';
if (w) { ... } // true if it owns something
} // Widget deleted here automatically
- Zero overhead compared to a raw pointer.
- Cannot be copied — there can only be one owner.
- Can be moved — ownership transfers:
auto a = std::make_unique<Widget>(1);
auto b = a; // ERROR: copy is deleted
auto c = std::move(a); // OK: c owns it now; a is empty (nullptr)
Transferring ownership through functions¶
// Factory: returns ownership to the caller
std::unique_ptr<Widget> make_widget(int id) {
return std::make_unique<Widget>(id); // no std::move needed on return
}
// Sink: takes ownership
void store(std::unique_ptr<Widget> w) {
registry.push_back(std::move(w));
}
store(make_widget(3));
auto w = make_widget(4);
store(std::move(w)); // explicit: "I'm giving this away"
Other operations¶
w.get(); // raw, non-owning pointer (still owned by w)
w.reset(); // delete the object now; w becomes empty
w.reset(new ...); // replace (prefer: w = std::make_unique<...>())
w.release(); // give up ownership WITHOUT deleting — you must delete it. Rare.
std::unique_ptr<T[]> exists for arrays, but just use std::vector.
std::shared_ptr: shared ownership¶
Several owners; the object is deleted when the last one goes away. It keeps a reference count in a separately allocated control block.
auto a = std::make_shared<Widget>(1);
std::cout << a.use_count() << '\n'; // 1
{
auto b = a; // copy: both own it
std::cout << a.use_count() << '\n'; // 2
}
std::cout << a.use_count() << '\n'; // 1
Costs: an extra allocation (merged into one by make_shared), atomic reference-count updates on every copy, and — worst — it makes lifetimes hard to reason about. Use shared_ptr only when ownership is genuinely shared (e.g. a cache entry used by several in-flight requests, a node referenced from multiple graphs). unique_ptr is the default.
std::weak_ptr: observe without owning¶
Two shared_ptrs that point at each other never reach a count of zero — a cycle leak:
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // cycle! neither node is ever freed
};
Break it with weak_ptr, which doesn't contribute to the count:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // non-owning back-link
};
if (auto p = node->prev.lock()) { // lock() → shared_ptr, empty if already deleted
use(*p);
}
Also useful for caches and observers that shouldn't keep objects alive.
Raw pointers and references still have a job¶
Smart pointers express ownership. For non-owning access, use references or raw pointers:
void print(const Widget& w); // needs a widget, doesn't care who owns it
void maybe_print(const Widget* w); // may be null, doesn't own it
auto owner = std::make_unique<Widget>(1);
print(*owner);
maybe_print(owner.get());
Don't pass smart pointers to functions that just use the object. void print(const std::unique_ptr<Widget>&) needlessly restricts callers.
Guidelines for function signatures¶
| Signature | Meaning |
|---|---|
void f(Widget&) / const Widget& |
uses the object; doesn't care about ownership |
void f(Widget*) |
uses it; may be null; doesn't own |
void f(std::unique_ptr<Widget>) |
takes ownership |
std::unique_ptr<Widget> f() |
gives ownership to the caller |
void f(std::shared_ptr<Widget>) |
will share ownership (keeps a copy) |
void f(const std::shared_ptr<Widget>&) |
might keep a copy; rarely what you want |
Always use make_unique / make_shared¶
auto p = std::make_unique<Widget>(1); // good
std::unique_ptr<Widget> q{new Widget{1}}; // works, but writes `new`
make_shared also performs one allocation instead of two.
Custom deleters¶
unique_ptr can manage non-memory resources too:
#include <cstdio>
struct FileCloser {
void operator()(std::FILE* f) const { std::fclose(f); }
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr f{std::fopen("data.txt", "r")};
if (!f) { /* failed to open */ }
Polymorphism and containers¶
The most common legitimate reason for heap allocation: a container of objects of different derived types (lesson 7):
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(1.0));
shapes.push_back(std::make_unique<Square>(2.0));
for (const auto& s : shapes) {
std::cout << s->area() << '\n';
}
Common mistakes¶
- Using
shared_ptrby default. Start withunique_ptr; upgrade only when ownership is truly shared. - Creating two smart pointers from one raw pointer — double delete:
- Using a
unique_ptrafter moving from it — it'snullptr. - Keeping
.get()pointers after the owner is destroyed — dangling. shared_ptrcycles — useweak_ptrfor back-references.- Passing smart pointers where a reference would do.
std::shared_ptr<T>(this)inside a member function — creates a second control block. Usestd::enable_shared_from_this.
Exercises¶
- Build a singly linked list of ints with
std::unique_ptr<Node> next. Implementpush_front,print, andreverse. What happens when you destroy a list with 1,000,000 nodes? (Hint: recursive destructors — fix it with an iterative destructor.) - Write a factory
std::unique_ptr<Shape> make_shape(std::string_view name)that returnsnullptrfor unknown names. - Demonstrate a
shared_ptrcycle leak with a parent/child pair, confirm it with-fsanitize=address(LeakSanitizer), then fix it withweak_ptr. - Wrap a C API pair like
std::fopen/std::fclosein aunique_ptrwith a lambda deleter:std::unique_ptr<std::FILE, decltype(&std::fclose)>. - Refactor a function that takes
const std::unique_ptr<Widget>&to takeconst Widget&. Why is the new version more reusable?
Recap¶
- Smart pointers are RAII for single heap objects, and their type documents ownership.
std::unique_ptr— sole owner, zero cost, move-only. The default.std::shared_ptr— shared ownership via reference counting. Use deliberately.std::weak_ptr— non-owning observer; breaks cycles;lock()to use.- Non-owning access: plain references and raw pointers.
- Create with
std::make_unique/std::make_shared, never withnew.
Next: Classes: fundamentals →