std::shared_ptr<T> is a smart pointer that lets multiple owners share a single heap object and frees it only when the last owner goes away. It works by keeping a reference count: every copy of the pointer increments the count, and every destruction decrements it. When the count hits zero, the object is deleted. This chapter covers what shared_ptr is, how its control block works, what its member functions do, what it costs compared with std::unique_ptr, and the parts of its design that appear in interviews.
Most ownership in C++ is single-owner. One container, one function, one parent object owns the value, and that owner decides when it dies. std::unique_ptr models that case and is the right default.
Shared ownership is for the cases where it is not possible to say in advance which referent will be the last one alive. Consider an online store. A Product object is referenced from a customer's cart and also from their wishlist. The customer could empty the cart first, or remove the wishlist entry first. Whichever happens last is the one that should free the product. Neither side can be the owner because neither side can promise to outlive the other.
Three shared_ptrs pointed at the same Product: one local variable, one in the cart, one in the wishlist. The count dropped as containers were emptied, and the destructor only ran when the local variable went out of scope at the end of main, where the count finally reached zero. No call to delete anywhere, and no risk of double-freeing the product when both containers happen to hold the last copy.
std::make_shared<T>(args...) (C++11) is the preferred way to build a shared_ptr. The short version: it allocates the object and the bookkeeping in a single allocation. Use it unless there is a specific reason not to.
A shared_ptr is more than a pointer. To make reference counting work, it carries a second pointer to a small heap-allocated structure called the control block. Every shared_ptr that owns the same object shares the same control block.
The control block holds:
shared_ptr instances currently owning the object. When this drops to zero, the object's destructor runs.std::weak_ptr instances observing the object, plus one for "the object is still alive." When the strong count hits zero the object is destroyed, but the control block itself only goes away when the weak count also reaches zero.delete ptr, but you can supply your own.The diagram shows three shared_ptr variables pointing at the same Product on the heap, all wired to the same control block. Each shared_ptr itself is two pointer-sized fields: one for the managed object, one for the control block. The control block is small but distinct from the object, and it lives on the heap independently.
The "weak count is strong-count-plus-one when the object is alive" detail matters for an edge case. As long as there are any shared_ptrs, the weak count is at least 1. When the last shared_ptr dies, the strong count hits zero, the object is destroyed, and the weak count is decremented from "1 + any weak_ptrs" to "any weak_ptrs". If no weak_ptrs exist, the control block is freed immediately too. If weak_ptrs do exist, the control block survives until they are all gone, which lets them safely detect that the object is dead.
There are three common ways to construct a shared_ptr. They are not equivalent.
A shared_ptr can be handed a freshly-new-ed pointer to take over.
This compiles and runs, but it has a subtle cost. The new Product(...) allocates the product on the heap. Then the shared_ptr constructor allocates the control block separately. That is two heap allocations for a single shared object.
There is also a safety issue. With foo(std::shared_ptr<A>(new A), std::shared_ptr<B>(new B)), if new B throws after new A succeeds, the raw pointer to A leaks because no one has wrapped it yet. The construction from raw pointer cannot apply RAII to the raw allocation it did not make.
std::make_sharedThe preferred way:
std::make_shared allocates the object and the control block in one contiguous heap block. That is one allocation instead of two, the object data sits next to its control block for better cache behavior, and there is no leak window. The main trade-off is that the memory is not released until the last weak_ptr also dies, which can matter when the object is large.
shared_ptrCopying shares ownership. Moving transfers it.
The copy bumps the strong count from 1 to 2. The move from a to c does not change the count: a hands off its ownership to c and becomes empty (its internal pointers are null), so the total number of owning shared_ptrs is still 2.
Copying a shared_ptr performs an atomic increment on the strong count, and destroying one performs an atomic decrement. Moves do not touch the count. Prefer to move shared_ptrs when an extra owner is not needed, especially across thread boundaries, where the atomic operations are most expensive.
A shared_ptr exposes a small interface. Most code uses only a handful of these.
| Member | What it does |
|---|---|
get() | Returns the underlying raw pointer. Does not transfer ownership. |
operator* | Dereferences and gives you T&. |
operator-> | Member access on the pointed-to object. |
operator bool() | True if the shared_ptr is non-empty. |
use_count() | Returns the current strong count. Mostly useful for debugging. |
unique() | Returns true if the strong count is 1. Deprecated in C++17, removed in C++20. Use use_count() == 1 instead. |
reset() | Drops ownership of the current object. Optional argument adopts a new one. |
swap(other) | Exchanges the managed object and control block with another shared_ptr. |
A note on get(): the returned raw pointer is only valid as long as some shared_ptr still owns the object. Never call delete on the result of get() (that would double-free), and never wrap it in another independent shared_ptr (that would create a second control block and free the same object twice).
This is the area that confuses people most often, so it is worth being exact.
The control block's strong and weak counts are atomic. Two threads can each hold their own shared_ptr to the same object and copy, destroy, or reset their own instances simultaneously without corrupting the count.
The pointed-to object is not made thread-safe by shared_ptr. If two threads call non-const methods on the same Product through shared_ptrs, a mutex or some other synchronization is still required. shared_ptr only protects its own bookkeeping.
The shared_ptr instance itself (the local variable holding the two pointers) is not internally synchronized. If two threads both modify the same shared_ptr variable (one calls reset() while the other copies it), that is a data race. C++20 added std::atomic<std::shared_ptr<T>> for cases where a shared shared_ptr variable is mutated by multiple threads.
The lambda captures p by value, so each thread gets its own shared_ptr instance. The strong count goes up to 5 at peak (one outside, four inside the threads) and decrements as each thread finishes. The atomic operations on the count are what make that safe.
By default, a shared_ptr calls delete on the managed pointer. A different deleter can handle resources that need different cleanup. The deleter is stored in the control block, so it does not affect the static type of the shared_ptr.
The lambda runs when the last shared_ptr to the FILE* is destroyed. The type is still std::shared_ptr<FILE>; the deleter does not appear in the template arguments. This is different from std::unique_ptr, where the deleter is part of the type. Storing it in the control block is what allows that.
shared_ptr has a constructor that takes two arguments: an existing shared_ptr (to share ownership of) and a raw pointer (to expose as the value). The result owns whatever the first argument owned, but get() returns the second.
This is useful for handing out a shared_ptr to a member of a larger object without giving up control of the outer object's lifetime.
The email shared_ptr shares the same control block as order, even though it dereferences to the string. The order survives until both shared_ptrs are gone. Use this rarely and only when a shared_ptr to part of an object whose whole lifetime is already being managed is actually needed.
unique_ptrstd::unique_ptr is one pointer wide, has zero runtime overhead beyond a function call to its deleter at destruction, and supports no copying. std::shared_ptr pays for what it offers.
| Property | unique_ptr<T> | shared_ptr<T> |
|---|---|---|
| Size | One pointer | Two pointers |
| Copy | Disabled | Allowed; atomic increment on the strong count |
| Move | Cheap pointer swap | Cheap pointer swap (no atomic op) |
| Destruction | Direct destructor + deleter call | Atomic decrement; if zero, destroy object; if weak count also zero, free control block |
| Extra heap allocation | None | One for the control block (avoided by make_shared, which fuses object + block) |
| Deleter location | Part of the type | Type-erased inside the control block |
| Thread-safe refcount | N/A | Yes |
Every copy of a shared_ptr and every destruction touches an atomic counter, even on the single-threaded path. The atomic op is cheap in absolute terms but not free. Without shared ownership, use unique_ptr to avoid the entire control block.
The rule of thumb most teams follow: default to unique_ptr. Use shared_ptr only when ownership is shared and no single component can outlive all the others.
reset, and the Order of Operationsreset() does two things in order: it drops the current object (decrementing the strong count, possibly destroying the object) and then optionally adopts a new one. Assignment from another shared_ptr does the same, with the new ownership coming from the right-hand side.
The assignment built the new Mouse Pad first, then dropped the old USB Cable. The reset() at the end dropped the Mouse Pad. The print order shows exactly when each destructor ran, which is often the easiest way to verify ownership.
shared_ptr interacts with several other features that have their own chapters in this section.
shared_ptrs that own each other create a cycle that the reference count alone cannot break. weak_ptr is the fix.The simple rule for now: when A and B need to refer to each other and both would be shared_ptr, hold off and use weak_ptr on one side instead.
10 quizzes