Lesson 4 of 6
Reference counting has one failure mode, and it is not obscure: a cycle never reaches zero.
If A owns B and B owns A, each keeps the other's count at 1 forever. Nothing external points at either, so nothing can ever free them.
#include <iostream>
#include <memory>
#include <string>
struct Person {
std::string name;
std::shared_ptr<Person> friend_of; // both directions own
Person(std::string n) : name(std::move(n)) {
std::cout << " created " << name << "\n";
}
~Person() { std::cout << " destroyed " << name << "\n"; }
};
int main() {
{
auto alice = std::make_shared<Person>("Alice");
auto bob = std::make_shared<Person>("Bob");
alice->friend_of = bob; // Bob's count -> 2
bob->friend_of = alice; // Alice's count -> 2
std::cout << " alice count: " << alice.use_count() << "\n";
std::cout << " bob count: " << bob.use_count() << "\n";
} // locals go away; each count drops to 1, not 0
std::cout << "scope ended — note what did NOT print\n";
return 0;
}
created Alice
created Bob
alice count: 2
bob count: 2
scope ended — note what did NOT print
No destructors ran. Both objects leaked, and the program exited cleanly reporting nothing — the
same silence as lesson 8.4, except shared_ptr was supposed to prevent exactly this.
This is worth sitting with: smart pointers do not make leaks impossible. They make the common leaks impossible. A cycle is the one they cannot see.
std::weak_ptr
A weak_ptr observes an object without owning it. It does not affect the strong count, so it
does not keep anything alive.
#include <iostream>
#include <memory>
#include <string>
struct Person {
std::string name;
std::weak_ptr<Person> friend_of; // observes, does not own
Person(std::string n) : name(std::move(n)) {
std::cout << " created " << name << "\n";
}
~Person() { std::cout << " destroyed " << name << "\n"; }
};
int main() {
{
auto alice = std::make_shared<Person>("Alice");
auto bob = std::make_shared<Person>("Bob");
alice->friend_of = bob;
bob->friend_of = alice;
std::cout << " alice count: " << alice.use_count() << "\n";
std::cout << " bob count: " << bob.use_count() << "\n";
// To use a weak_ptr you must lock it, which yields a shared_ptr
// that is null if the object is already gone.
if (auto f = alice->friend_of.lock()) {
std::cout << " Alice's friend is " << f->name << "\n";
}
}
std::cout << "scope ended\n";
return 0;
}
created Alice
created Bob
alice count: 1
bob count: 1
Alice's friend is Bob
destroyed Bob
destroyed Alice
scope ended
Both counts stayed at 1, and both objects were destroyed — note they die at the closing brace, before the line after it runs. Same program, one word changed.
lock() is the whole interface
You cannot dereference a weak_ptr directly. That is deliberate: between checking and using, the
object could die. lock() does both atomically, returning a shared_ptr that is either valid or
null.
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> observer;
{
auto owner = std::make_shared<int>(42);
observer = owner;
std::cout << "expired: " << observer.expired() << "\n";
if (auto locked = observer.lock()) {
std::cout << "value: " << *locked << "\n";
std::cout << "count while locked: " << locked.use_count() << "\n";
}
} // owner gone
std::cout << "expired: " << observer.expired() << "\n";
if (auto locked = observer.lock()) {
std::cout << "still here\n";
} else {
std::cout << "object is gone; lock() returned null\n";
}
return 0;
}
expired: 0
value: 42
count while locked: 2
expired: 1
object is gone; lock() returned null
Note count while locked: 2 — locking creates a genuine temporary owner, which is what makes the
object safe to use for as long as you hold it.
expired() exists, but is only useful for a hint: it can become stale the instant it returns.
Always use lock() and check the result rather than testing expired() and then locking.
The parent–child rule
The cycle above was contrived. The one you will actually meet is a tree where children need to reach their parent:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Node {
std::string name;
std::vector<std::shared_ptr<Node>> children; // parent owns children
std::weak_ptr<Node> parent; // child observes parent
Node(std::string n) : name(std::move(n)) {}
~Node() { std::cout << " destroyed " << name << "\n"; }
};
void add_child(const std::shared_ptr<Node>& parent, const std::shared_ptr<Node>& child) {
parent->children.push_back(child);
child->parent = parent;
}
int main() {
{
auto root = std::make_shared<Node>("root");
auto leaf = std::make_shared<Node>("leaf");
add_child(root, leaf);
if (auto p = leaf->parent.lock()) {
std::cout << "leaf's parent is " << p->name << "\n";
}
std::cout << "root has " << root->children.size() << " child\n";
}
std::cout << "tree released\n";
return 0;
}
leaf's parent is root
root has 1 child
destroyed root
destroyed leaf
tree released
The rule generalises: ownership points one way; back-references are weak. Parent owns child,
child observes parent. Container owns element, element observes container. Make both directions
shared_ptr and you have built a leak.
The other use: caches and observers
weak_ptr also expresses "I want this if it still exists, and I do not want to keep it alive":
#include <iostream>
#include <memory>
#include <string>
struct Session {
std::string user;
Session(std::string u) : user(std::move(u)) {}
~Session() { std::cout << " session ended for " << user << "\n"; }
};
int main() {
std::weak_ptr<Session> cached;
{
auto active = std::make_shared<Session>("sokha");
cached = active;
if (auto s = cached.lock()) {
std::cout << "cache hit: " << s->user << "\n";
}
} // the session ends when its real owner goes
if (auto s = cached.lock()) {
std::cout << "cache hit: " << s->user << "\n";
} else {
std::cout << "cache miss: session already ended\n";
}
return 0;
}
cache hit: sokha
session ended for sokha
cache miss: session already ended
A cache holding shared_ptrs never lets anything go. A cache holding weak_ptrs lets the real
owner decide, and simply misses afterwards. That is usually what you want.
Try it
Build a two-node cycle with shared_ptr and confirm the destructors never run. Then change one
side to weak_ptr and confirm both are destroyed. Finally, ask which side should be the weak
one in a parent–child tree, and why the other choice is wrong.
Checkpoint
- Reference counting cannot free a cycle: each side keeps the other's count above zero.
- Smart pointers prevent the common leaks, not every leak.
std::weak_ptrobserves without owning and does not affect the strong count.- You cannot dereference it; call
lock(), which returns ashared_ptror null. - Prefer
lock()overexpired()— the latter can be stale immediately. - Ownership points one way; back-references are weak. Parent owns child, child observes parent.
- A
weak_ptrcache lets the real owner control the lifetime and simply misses afterwards.
Questions about this lesson
Log in to ask a question.