std::shared_ptr and std::weak_ptr

std::shared_ptr<T> is a pointer with a reference count: every copy increments it, every destruction decrements it, and the object is deleted when the last owner goes away. It is the tool for an object that several parts of a program really share.

The count is the whole story, and it has one failure mode: two objects that own each other keep the count above zero forever. This lesson builds the count up and down, then shows the cycle and the weak_ptr that breaks it.

make_shared and the count

std::make_shared<int>(42) allocates the int and the control block that holds the count in one allocation. use_count() returns 1 for a fresh pointer.

The program prints the value and the count. The count is part of the control block, not of the object, and every copy of the pointer shares it.

make_shared is preferred to shared_ptr<int>(new int{42}) for the same reason as make_unique: one expression for the allocation and the ownership, and one allocation instead of two.

#include <iostream> #include <memory> int main() { auto p = std::make_shared<int>(42); std::cout << *p << ' ' << p.use_count() << '\n'; }

g++ -std=c++23 -Wall -Wextra first.cpp -o first
./first

42 1

A copy shares

Copying a shared_ptr is cheap: it increments the count and copies the two pointers. The program copies one into a block and prints the count inside and outside, which is 2 and then 1.

The object is deleted when the count reaches zero, which happens when the last pointer in the block goes out of scope. The print after the block shows the count back at 1.

A shared_ptr passed by value adds an owner for the duration of the call. Passing it by const reference is the cheaper read, because it does not touch the count.

#include <iostream> #include <memory> int main() { auto a = std::make_shared<int>(7); { auto b = a; std::cout << a.use_count() << ' '; } std::cout << a.use_count() << '\n'; }

g++ -std=c++23 -Wall -Wextra share.cpp -o share
./share

2 1

Dropping an owner

reset() releases one ownership and leaves the pointer null. The object survives as long as another shared_ptr still points at it.

The program makes two owners and resets the second. The count for the first is still 1, and the second pointer is null, which the print shows.

Assigning nullptr and letting a pointer go out of scope do the same. The count is decremented in the shared_ptr destructor, so an owner that is forgotten in a container keeps the object alive.

#include <iostream> #include <memory> int main() { auto a = std::make_shared<int>(1); auto b = a; b.reset(); std::cout << a.use_count() << ' ' << (b == nullptr) << '\n'; }

g++ -std=c++23 -Wall -Wextra reset.cpp -o reset
./reset

1 1

A weak_ptr watches

A weak_ptr points at the object without owning it, so it does not change the count. lock() returns a shared_ptr when the object is alive and an empty one when it is gone, and expired() answers the same question without locking.

The program watches an object, locks it while it is alive and checks the weak pointer after the owner is reset. The counts and the value are printed in one line.

A weak_ptr is the tool for a cache and for a back reference. It never extends a lifetime, which is exactly what the next sections need.

#include <iostream> #include <memory> int main() { auto owner = std::make_shared<int>(5); std::weak_ptr<int> watcher = owner; std::cout << owner.use_count() << ' ' << !watcher.expired() << ' '; if (auto locked = watcher.lock()) { std::cout << *locked << ' '; } owner.reset(); std::cout << watcher.expired() << '\n'; }

g++ -std=c++23 -Wall -Wextra weak.cpp -o weak
./weak

1 1 5 1

A cycle keeps itself alive

Two nodes that own each other with shared_ptr have counts of 2. Resetting the local pointers drops each to 1, and neither reaches zero, so neither node is ever deleted.

The program builds the cycle and prints the counts before the locals are reset. The counts are 2 and 2, and after reset they are still 1 and 1: the two objects hold each other.

This is the one leak that reference counting cannot see. The memory is not lost by a bug in the count; it is held by an object that is only reachable from the cycle.

#include <iostream> #include <memory> struct node { std::shared_ptr<node> next; }; int main() { auto a = std::make_shared<node>(); auto b = std::make_shared<node>(); a->next = b; b->next = a; std::cout << a.use_count() << ' ' << b.use_count() << '\n'; }

g++ -std=c++23 -Wall -Wextra cycle.cpp -o cycle
./cycle

2 2

weak_ptr breaks the cycle

Making one edge weak breaks the cycle: the back edge does not own, so a has one owner and b has two, its local variable and the forward edge from a. When the locals go away both counts reach zero.

The program stores the forward edge as shared_ptr and the back edge as weak_ptr, prints the two counts, resets a and asks whether the back edge is expired. The answer is 1, and both nodes were freed.

The rule of thumb is that the owning direction is the one that describes the lifetime, and every back reference is weak. A parent that owns its children and children that point at the parent is the usual shape.

#include <iostream> #include <memory> struct node { std::shared_ptr<node> next; std::weak_ptr<node> back; }; int main() { auto a = std::make_shared<node>(); auto b = std::make_shared<node>(); a->next = b; b->back = a; std::cout << a.use_count() << ' ' << b.use_count() << ' '; a.reset(); std::cout << b->back.expired() << '\n'; }

g++ -std=c++23 -Wall -Wextra break.cpp -o break_cycle
./break_cycle

1 2 1

Exercises

  1. Store three shared_ptr copies of one object in a vector and print use_count after each push_back.
  2. Write a cache class that holds objects in a map of weak_ptr and reloads a missing one.
  3. Draw the ownership edges of a tree with a parent pointer and decide which edge must be weak.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does use_count() return for a freshly created shared_ptr?

    Answer

    1

    The pointer is one owner from the moment it is created, so the count starts at 1.

  2. What does this program print? auto a = std::make_shared<int>(7); { auto b = a; std::cout << a.use_count() << ' '; } std::cout << a.use_count();

    Answer

    2 1

    The copy inside the block raises the count to 2, and its destruction brings it back to 1.

  3. What does b.reset() do when b is one of two owners?

    Answer

    Drops b's ownership and leaves a with count 1

    reset releases one owner; the object lives while another shared_ptr holds it.

  4. What does weak_ptr::lock() return when the object is gone?

    Answer

    An empty shared_ptr

    The locked pointer is empty, which the caller tests before using it.

  5. Two nodes own each other with shared_ptr. What are their counts after the local pointers are reset?

    Answer

    1 and 1

    Each node is still owned by the other, so neither count reaches zero and neither is deleted.

  6. Which edge of a parent-child pair should be the weak one?

    Answer

    The child's pointer back to the parent

    The parent owns the child; the back reference must not own, or the two keep each other alive.

  7. What does this program print? auto a = std::make_shared<int>(5); std::weak_ptr<int> w = a; a.reset(); std::cout << w.expired();

    Answer

    1

    The weak pointer does not own, so the object is deleted when a is reset and the weak pointer expires.

  8. How many allocations does std::make_shared<int>(42) make on this build?

    Answer

    One, for the object and the control block together

    make_shared allocates the object and the control block in one block, unlike the shared_ptr(new T) form.

  9. Why is passing a shared_ptr by const reference cheaper than by value?

    Answer

    The reference avoids the atomic increment of the count

    A copy touches the shared count, which is atomic; a const reference does not.

  10. When should a function take a shared_ptr rather than a reference to the object?

    Answer

    When it has to keep the object alive after it returns

    The pointer is an ownership handle; a function that does not store it only needs the object.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev