A smart pointer is not "a safer pointer". It is a statement about ownership: who is responsible for destroying an object, and when. Get the ownership design right and the pointer types follow; get it wrong and no pointer type will save you. This lesson is about making that decision deliberately.
1. Three relationships
Every pointer-ish thing in your program is one of these:
| Relationship | Meaning | Spell it |
|---|---|---|
| Unique owner | exactly one object is responsible; ownership can move | std::unique_ptr<T> (or just a T member) |
| Shared owner | several owners; the last one out destroys it | std::shared_ptr<T> |
| Borrower / observer | uses the object, never destroys it, must not outlive it | T&, const T&, T*, std::span, std::string_view |
| Weak observer | wants to know whether it still exists | std::weak_ptr<T> |
The default is unique. Shared ownership is a real design choice with real costs (an atomic count, a control block, lifetimes nobody can predict), not a way to avoid thinking about lifetimes.
Raw pointers aren't evil — owning raw pointers are. A T* that means
"I'm looking at it, someone else owns it" is perfectly modern C++.
2. std::unique_ptr: one owner, zero overhead
#include <memory>
#include <print>
#include <string>
#include <utility>
struct Texture {
std::string name;
explicit Texture(std::string n) : name(std::move(n)) { std::println("load {}", name); }
~Texture() { std::println("free {}", name); }
};
int main() {
auto a = std::make_unique<Texture>("grass"); // owner: a
std::unique_ptr<Texture> b = std::move(a); // ownership moves; a is now null
std::println("a is {}", a ? "set" : "null");
b.reset(); // destroys grass now
std::println("end of main");
}Texture never moves in memory; only which pointer owns it changes. reset() destroys it on the spot.sizeof(std::unique_ptr<T>)equalssizeof(T*)with the default deleter: it costs nothing over a raw pointer, and the compiler writes thedeletefor you.- It's move-only. Copying would mean two owners — exactly what it forbids.
std::make_unique<T>(args...)is the way to create one. There's no nakednewto forget to match.unique_ptr<T[]>owns an array (make_unique<T[]>(n)), usesdelete[], and providesoperator[]. Usually astd::vectoris better.
Custom deleters: owning things that aren't newed
unique_ptr can own any resource with a "release" function: C handles, file
descriptors, library objects.
#include <memory>
#include <print>
struct Handle { int id; };
Handle* open_handle(int id) { std::println("open {}", id); return new Handle{id}; }
void close_handle(Handle* h) { std::println("close {}", h->id); delete h; }
int main() {
// The deleter type is part of the unique_ptr type.
std::unique_ptr<Handle, decltype(&close_handle)> h(open_handle(7), &close_handle);
std::println("using {}", h->id);
} // close 7A stateless lambda or function object deleter (struct Closer { void operator()(Handle*) const; };) keeps the unique_ptr the size of a pointer;
a function pointer deleter doubles it.
3. Passing smart pointers around (GotW #91)
The parameter type says what the function does with ownership:
| Parameter | Means |
|---|---|
Widget& / const Widget& |
uses the widget; doesn't care how it's owned — the default |
Widget* |
same, but "no widget" is allowed |
std::unique_ptr<Widget> (by value) |
takes ownership (caller must std::move) |
std::unique_ptr<Widget>& |
may reseat the caller's pointer (rare) |
std::shared_ptr<Widget> (by value) |
will keep a share (stores it somewhere) |
const std::shared_ptr<Widget>& |
might take a share, maybe not (rare) |
The commonest mistake is void draw(std::shared_ptr<Widget> w) for a function
that only draws: it forces callers to have a shared_ptr, and costs two atomic
operations per call for nothing. Take const Widget&.
#include <memory>
#include <print>
#include <vector>
struct Widget { int id; };
void draw(const Widget& w) { std::println("draw {}", w.id); } // borrows
void keep(std::vector<std::unique_ptr<Widget>>& owners,
std::unique_ptr<Widget> w) { owners.push_back(std::move(w)); } // takes ownership
int main() {
std::vector<std::unique_ptr<Widget>> owners;
auto w = std::make_unique<Widget>(1);
draw(*w); // borrow: dereference, no ownership involved
keep(owners, std::move(w)); // transfer: the call site says so
draw(*owners.front());
}4. std::shared_ptr: shared ownership and its costs
A shared_ptr points at the object and at a control block holding the
strong count, the weak count and the deleter. Copying increments the strong
count (atomically); destroying a copy decrements it; at zero, the object is
destroyed. The control block lives until the weak count is zero too.
#include <memory>
#include <print>
int main() {
auto a = std::make_shared<int>(42);
std::println("count {}", a.use_count()); // 1
{
auto b = a; // copy: +1
std::println("count {}", a.use_count()); // 2
} // b destroyed: -1
std::println("count {}", a.use_count()); // 1
}make_shared, both live in one allocation — so the object's memory is only freed when the last weak reference goes too.make_shared allocates once: object and control block in a single block.
std::shared_ptr<T>(new T) allocates twice. The flip side: with make_shared,
the object's memory can't be returned until the last weak_ptr is gone too.
Thread safety: the reference count is thread-safe — copies of the same
shared_ptr may be created and destroyed on different threads. The object
is not protected at all, and neither is a single shared_ptr variable being
assigned from two threads (that needs std::atomic<std::shared_ptr<T>>).
The aliasing constructor shares ownership of one object while pointing at another — typically a member:
#include <memory>
#include <print>
#include <string>
struct Config { std::string name = "prod"; int port = 443; };
int main() {
auto cfg = std::make_shared<Config>();
std::shared_ptr<int> port(cfg, &cfg->port); // keeps *cfg alive, points at port
cfg.reset();
std::println("port {} still valid; owners {}", *port, port.use_count());
}The cycle trap
Shared ownership forms a graph. If the graph has a cycle, the counts never reach zero and everything in the cycle leaks:
struct Person {
std::shared_ptr<Person> partner; // owning in both directions: a cycle
};
auto a = std::make_shared<Person>();
auto b = std::make_shared<Person>();
a->partner = b;
b->partner = a; // a and b now keep each other alive forevera and b go out of scope, each Person in the left cycle still has strong count 1 — from the other one — so neither is ever destroyed. On the right, the weak edge doesn't count: bo's count reaches 0 and the pair is freed.LeakSanitizer usually reports this at exit — but its scan is conservative (a
stale pointer left on the stack can make the cycle look reachable), so don't
rely on it: the reliable test is a weak_ptr observer that should have expired.
The fix is to decide that one direction doesn't own.
5. std::weak_ptr: observing without owning
A weak_ptr is created from a shared_ptr and doesn't keep the object alive.
To use the object you must lock it, which gives you a shared_ptr that's
either null (the object is gone) or keeps it alive while you use it:
#include <memory>
#include <print>
#include <string>
struct Person {
std::string name;
std::weak_ptr<Person> partner; // observes, doesn't own: no cycle
};
int main() {
auto a = std::make_shared<Person>("ada");
{
auto b = std::make_shared<Person>("bo");
a->partner = b;
b->partner = a;
if (auto p = a->partner.lock()) std::println("{}'s partner is {}", a->name, p->name);
} // b's last owner is gone: bo is destroyed
if (a->partner.expired()) std::println("{}'s partner is gone", a->name);
}Always lock() and test the result. Checking expired() and then locking is
a race in multithreaded code (the object can die in between); lock() is the
one atomic step.
Caches are the other classic use: hold weak_ptrs to resources so the
cache never keeps something alive that nobody uses.
6. Worked example: a tree with back pointers
A tree has a natural ownership direction: parents own children. Children need
to know their parent, but must not own it — that would be a cycle. So:
children in unique_ptr, parent as a plain observing pointer.
#include <memory>
#include <print>
#include <string>
#include <utility>
#include <vector>
class Node {
public:
explicit Node(std::string name, Node* parent = nullptr)
: name_(std::move(name)), parent_(parent) {}
Node& add_child(std::string name) {
children_.push_back(std::make_unique<Node>(std::move(name), this));
return *children_.back(); // a borrow: *this keeps owning it
}
std::string path() const {
return parent_ ? parent_->path() + "/" + name_ : name_;
}
private:
std::string name_;
Node* parent_; // observer: never deletes
std::vector<std::unique_ptr<Node>> children_; // owners
};
int main() {
Node root("root");
Node& src = root.add_child("src");
Node& main_cpp = src.add_child("main.cpp");
std::println("{}", main_cpp.path());
} // root destroys src, which destroys main.cpp — no manual cleanupunique_ptr children — the direction that owns and destroys. Dashed: the raw Node* parent_ back pointers, which never delete. No cycle of ownership, so destroying root frees everything.Why unique_ptr<Node> in the vector rather than vector<Node>? Children hand
out Node& and store this as their children's parent pointer. A
vector<Node> would move nodes when it grows, invalidating all of those.
unique_ptr keeps every node at a stable address.
Faded example — your turn
Write detach_child(i): remove child i from this node and return ownership of
it to the caller. What must happen to the detached node's parent pointer, and
why?
std::unique_ptr<Node> detach_child(std::size_t i) {
std::unique_ptr<Node> child = /* ??? */;
children_.erase(children_.begin() + i);
/* ??? */
return child;
}Answer
std::move(children_[i]) takes ownership out of the vector (leaving a null
unique_ptr that erase then removes), and child->parent_ = nullptr —
otherwise the detached node points at a parent that may be destroyed while the
caller still holds the child, and path() would read freed memory.
7. enable_shared_from_this — and its trap
Sometimes an object managed by shared_ptr needs to hand out a shared_ptr to
itself (say, to register a callback that keeps it alive). Writing
std::shared_ptr<T>(this) is a bug: it creates a second, independent control
block, so the object is deleted twice. Instead, derive from
std::enable_shared_from_this<T> and call shared_from_this():
#include <memory>
#include <print>
struct Session : std::enable_shared_from_this<Session> {
std::shared_ptr<Session> self() { return shared_from_this(); }
};
int main() {
auto s = std::make_shared<Session>();
auto t = s->self(); // shares s's control block
std::println("owners {}", s.use_count()); // 2
}shared_from_this() finds and shares the existing block instead.The trap: shared_from_this() only works if the object is already owned by
a shared_ptr. Called on a stack object, or from the constructor, it throws
std::bad_weak_ptr. Make such types creatable only through a factory that
returns a shared_ptr.
8. Preview: C++26 value-semantic owners
unique_ptr gives you an owning pointer whose copy is forbidden. Often you
actually want a member that's heap-allocated (for an incomplete type, or a
large or polymorphic object) but copies like a value. C++26 adds
std::indirect<T> (deep-copying owner of a T) and std::polymorphic<T>
(deep-copying owner of something derived from T):
#include <memory>
#include <print>
int main() {
std::indirect<int> a(5);
std::indirect<int> b = a; // deep copy: a new int on the heap
*b = 6;
std::println("{} {}", *a, *b);
}They're covered properly in Level 6 alongside std::variant.
Summary
- Decide the ownership first: unique (default), shared (only if truly shared), borrowed (references, raw pointers, views).
unique_ptrcosts nothing and moves;make_uniqueto create; custom deleters for non-newresources.- Parameters:
const T&to use,unique_ptr<T>by value to take ownership,shared_ptr<T>by value only to keep a share. shared_ptrcosts a control block and atomic counts;make_sharedallocates once; the count is thread-safe, the object isn't.- Cycles of
shared_ptrleak: make back-referencesweak_ptr(or raw observers when the lifetime is structurally guaranteed, as in a tree). weak_ptr::lock()and test, in one step. Nevershared_ptr<T>(this).