Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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_ptr observes without owning and does not affect the strong count.
  • You cannot dereference it; call lock(), which returns a shared_ptr or null.
  • Prefer lock() over expired() — the latter can be stale immediately.
  • Ownership points one way; back-references are weak. Parent owns child, child observes parent.
  • A weak_ptr cache lets the real owner control the lifetime and simply misses afterwards.

Questions about this lesson

Log in to ask a question.