By the end of this chapter you can
- Choose between unique_ptr, shared_ptr, and a raw pointer
- Explain the cost of a shared_ptr's control block
- Break a reference cycle with weak_ptr
Part 3 kept pointing at these and never delivered them. Every time a chapter said “put ownership in an automatic object and cleanup becomes unskippable”, the tool it had in mind was a smart pointer.
A smart pointer is RAII applied to a heap allocation: an object that holds a pointer and deletes it in its destructor. That is the whole idea. What takes a chapter is knowing which one to reach for, and when the answer is neither.
unique_ptr: one owner
std::unique_ptr<T> owns a heap object and deletes it when it goes out of
scope. It cannot be copied — that would mean two owners — but it can be moved,
which transfers ownership.
#include <iostream>
#include <memory>
#include <string>
struct Widget {
std::string name;
explicit Widget(std::string n) : name(std::move(n)) {
std::cout << " + " << name << " constructed\n";
}
~Widget() { std::cout << " - " << name << " destroyed\n"; }
};
int main() {
std::cout << "entering scope\n";
{
auto widget = std::make_unique<Widget>("alpha");
std::cout << " using " << widget->name << '\n';
} // deleted here, on every exit path
std::cout << "left scope\n";
}Use std::make_unique<T>(args...) rather than std::unique_ptr<T>(new T(...)).
It is shorter, it never leaves a raw new unowned for even an instant, and it
does not repeat the type.
The pointer behaves like a pointer — *p, p->member, and a bool conversion
for testing — and costs exactly what a raw pointer costs:
#include <iostream>
#include <memory>
int main() {
std::cout << "int* " << sizeof(int*) << " bytes\n";
std::cout << "unique_ptr<int> " << sizeof(std::unique_ptr<int>) << " bytes\n";
std::cout << "shared_ptr<int> " << sizeof(std::shared_ptr<int>) << " bytes\n";
std::cout << "weak_ptr<int> " << sizeof(std::weak_ptr<int>) << " bytes\n";
}unique_ptr is genuinely free: same size as a raw pointer, and the delete
compiles to the same instruction you would have written. There is no reason to
manage a single-owner heap object by hand.
Moving, not copying
#include <memory>
int main() {
auto a = std::make_unique<int>(1);
auto b = a; // error: the copy constructor is deleted
return *b;
}The error is the design. To hand ownership on, move it:
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
void consume(std::unique_ptr<int> owned) {
std::cout << " consume() now owns " << *owned << '\n';
} // deleted here
int main() {
auto value = std::make_unique<int>(42);
std::vector<std::unique_ptr<int>> owners;
owners.push_back(std::move(value)); // the vector owns it now
std::cout << "value is " << (value ? "still set" : "empty") << '\n';
std::cout << "vector holds " << *owners[0] << '\n';
consume(std::move(owners[0]));
std::cout << "element is " << (owners[0] ? "still set" : "empty") << '\n';
}A unique_ptr parameter taken by value says, in the signature, this function
takes ownership. A caller cannot pass one without writing std::move, so
transfer of ownership is visible at the call site. That is a documentation
property the language enforces.
Where unique_ptr earns its keep
Not for every heap object — for objects whose type is not known at compile time. Runtime polymorphism needs a pointer, and that pointer needs an owner:
#include <iostream>
#include <memory>
#include <vector>
struct Shape {
virtual ~Shape() = default; // required: see below
virtual double area() const = 0;
};
struct Circle : Shape {
double r;
explicit Circle(double radius) : r(radius) {}
double area() const override { return 3.14159265 * r * r; }
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
double area() const override { return side * side; }
};
int main() {
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));
double total = 0;
for (const auto& shape : shapes) total += shape->area();
std::cout << "total area " << total << '\n';
}shared_ptr: shared ownership, at a price
std::shared_ptr<T> lets several owners share one object; it is destroyed when
the last of them goes away. It does this by keeping a control block next to
the object holding a reference count.
#include <iostream>
#include <memory>
struct Resource {
Resource() { std::cout << " + acquired\n"; }
~Resource() { std::cout << " - released\n"; }
};
int main() {
auto first = std::make_shared<Resource>();
std::cout << "count " << first.use_count() << '\n';
{
auto second = first; // a copy: count goes up
std::cout << "count " << first.use_count() << '\n';
} // second dies: count goes down
std::cout << "count " << first.use_count() << '\n';
std::cout << "leaving main\n";
}The object is released exactly once, when the count reaches zero. That is what you are buying. What you are paying:
- Two allocations, or one bigger one.
make_sharedputs the object and the control block in a single allocation, which is why it is preferred overstd::shared_ptr<T>(new T). - An atomic increment and decrement per copy. The count must be thread-safe,
so copying a
shared_ptris not free even when only one thread exists. - Twice the pointer size, as the
sizeofoutput above showed: one pointer to the object, one to the control block.
#include <chrono>
#include <iostream>
#include <memory>
int main() {
constexpr int rounds = 2'000'000;
using clock = std::chrono::steady_clock;
using ms = std::chrono::milliseconds;
auto shared = std::make_shared<int>(1);
auto unique = std::make_unique<int>(1);
auto start = clock::now();
long long sink = 0;
for (int i = 0; i < rounds; ++i) {
auto copy = shared; // atomic ++ and --
sink += *copy;
}
auto mid = clock::now();
for (int i = 0; i < rounds; ++i) {
const int* observer = unique.get(); // no ownership, no counting
sink += *observer;
}
auto finish = clock::now();
std::cout << "shared_ptr copies: " << std::chrono::duration_cast<ms>(mid - start).count() << " ms\n";
std::cout << "raw observation: " << std::chrono::duration_cast<ms>(finish - mid).count() << " ms\n";
std::cout << "(checksum " << sink << ")\n";
}Reach for shared_ptr when ownership is genuinely shared and the last owner
is not knowable at compile time — a cache handing out entries, a node in a graph
with several parents, an object captured by several concurrent tasks. Otherwise
prefer one unique_ptr owner and non-owning observers.
Observing without owning
A raw pointer or reference is the right way to say “I use this and do not own
it”. Passing a shared_ptr where an observer is wanted forces a needless
atomic increment and, worse, silently extends the object’s lifetime.
#include <iostream>
#include <memory>
#include <string>
// Owns nothing. Cannot extend a lifetime. Works with any owner.
void print_name(const std::string& name) {
std::cout << " " << name << '\n';
}
int main() {
auto owned = std::make_unique<std::string>("from a unique_ptr");
auto shared = std::make_shared<std::string>("from a shared_ptr");
std::string local = "from a local";
print_name(*owned);
print_name(*shared);
print_name(local);
std::cout << "shared count is still " << shared.use_count() << '\n';
}The rule of thumb for parameters:
| The function… | Takes |
|---|---|
| only reads or mutates the object | const T& or T& |
| takes ownership | std::unique_ptr<T> by value |
| keeps a copy of the owner, sharing lifetime | std::shared_ptr<T> by value |
| may or may not receive an object | const T* (nullable, non-owning) |
Most functions are in the first row. A function signature full of smart pointers is usually a sign that ownership has not been thought about.
weak_ptr, and the cycle it breaks
Reference counting has one failure mode: two objects that own each other. Their counts never reach zero, and neither is ever destroyed.
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> partner;
~Node() { std::cout << " - node destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->partner = b; // a keeps b alive
b->partner = a; // and b keeps a alive
std::cout << "a.use_count() = " << a.use_count() << '\n';
std::cout << "leaving main — watch for the destructor messages\n";
}No destructor ran. Each node’s count was still 1 when its named shared_ptr
died, because the other node was still holding a reference. LeakSanitizer
reports both allocations — this is one leak the tools do catch.
std::weak_ptr is a non-owning reference to a shared_ptr-managed object. It
does not raise the count, so it does not keep the object alive; to use it you
must ask for a shared_ptr back, and that ask can fail:
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> owns; // strong, one direction
std::weak_ptr<Node> observes; // weak, the other
int id = 0;
~Node() { std::cout << " - node " << id << " destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->id = 1;
b->id = 2;
a->owns = b; // a keeps b alive
b->observes = a; // b only watches a
// Using a weak_ptr means asking whether the object is still there.
if (auto locked = b->observes.lock()) {
std::cout << "b can still see node " << locked->id << '\n';
}
std::cout << "a.use_count() = " << a.use_count() << '\n';
std::cout << "leaving main\n";
}Both destructors ran. The rule for cycles: make one direction strong and the other weak. Parent owns child, child observes parent. Cache owns entries, entries observe the cache.
lock() returning empty is not an error to be avoided — it is the answer to
“is it still alive?”, and it is why weak_ptr is safe where a raw pointer to a
possibly-dead object would be a use-after-free.
What not to do
#include <iostream>
#include <memory>
int main() {
int* raw = new int(7);
std::shared_ptr<int> first(raw); // control block A, count 1
std::shared_ptr<int> second(raw); // control block B, count 1 — separate!
std::cout << "first count " << first.use_count() << '\n';
std::cout << "second count " << second.use_count() << '\n';
std::cout << "both believe they are the only owner\n";
} // both delete: double freeConstructing two shared_ptrs from the same raw pointer creates two independent
counts, and both reach zero. This is the main reason to prefer make_shared and
make_unique: with no raw pointer in your code, there is nothing to hand to two
owners by mistake.
The same applies to get(). It returns a non-owning raw pointer for passing to
code that does not own — never for constructing another smart pointer, and never
for delete.
Choosing
- Does it need to be on the heap at all? A member, a local, or a
std::vectorelement is simpler, faster, and cannot leak. Most objects do not need dynamic allocation. Reach for the heap when the size is a run-time value, the object must outlive the scope that made it, or the type is polymorphic. - One owner?
unique_ptr. This is the default answer, and it costs nothing. - Genuinely several owners, none obviously last?
shared_ptr. - Using it but not owning it?
const T&,T&, or a rawT*if it may be absent. Never a smart pointer. - Breaking a cycle, or observing something that may die?
weak_ptr.