Recommended resources
The freely available resources this lesson will direct you to.
Hello! In the previous lesson, your UniqueFd wrapper made one responsibility explicit: exactly one object must close a descriptor, and moving the wrapper transfers that responsibility. Heap objects raise the same question, but an object graph adds another: which relationships should keep an object alive, and which should merely allow access to it?
This lesson gives you a method for choosing value ownership, std::unique_ptr, std::shared_ptr, or std::weak_ptr. The examples use components you might find in a message-processing service, where unnecessary allocation matters but a dangling observer is still a correctness bug. Allow about 40 minutes, including the selected reading and video segments.
Begin with the lifetime you want
Consider an engine that holds its current feed state. If the state should exist for exactly as long as the engine, the most direct representation is a member:
#include <cstdint>
struct FeedState {
std::uint64_t next_sequence = 0;
};
struct Engine {
FeedState state;
};
Engine owns state by value. Constructing an engine constructs its member; destroying the engine destroys the member. There is no separate allocation merely because one object owns another. A local FeedState would likewise have a lifetime governed by its scope.
Read the C++ Core Guidelines’ comparison of an unnecessarily heap-allocated object with a scoped object:
The C++ Core Guidelines’ rule R.5 establishes the default choice before smart pointers enter the picture: use an object directly when its lifetime already fits a scope or containing object.
In “R.5: Prefer scoped objects, don't heap-allocate unnecessarily,” read the Reason and the two examples, beginning with scoped ownership. Compare the cleanup obligations of new Gadget and the local Gadget g; the examples are alternatives, not two stages of one design.
Value ownership does not mean “no heap memory anywhere.” A std::vector<FeedState> is a value that owns its element storage, which may be dynamically allocated. What matters here is that the vector manages that storage itself; you do not need a separately allocated FeedState and a pointer for every element.
Direct ownership is particularly useful when processing many small records: it avoids per-object pointer indirection and may allow contiguous storage. But do not replace every pointer with a value mechanically. If several parts of a program must refer to the same identity, copying an object may create a different object rather than the relationship you intended.
Use unique_ptr when one owner needs indirection
Now suppose the engine selects a transport implementation at runtime. It still has one owner of that transport, but it cannot hold an abstract Transport implementation directly as a Transport value:
#include <memory>
struct Transport {
virtual ~Transport() = default;
virtual void poll() = 0;
};
struct Engine {
FeedState state; // Owned directly.
std::unique_ptr<Transport> transport; // Exclusively owned object.
};
Here, std::unique_ptr<Transport> expresses an owning relationship, not just the ability to call poll(). Destroying or resetting the pointer destroys its transport. The virtual destructor ensures that deleting a concrete transport through the base-class pointer destroys the complete object.
Watch David Olsen’s Back to Basics: C++ Smart Pointers on CppCon for the distinction between an exclusive owner and someone merely using an object:
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
David Olsen’s CppCon talk shows why std::unique_ptr is a natural extension of RAII for an object that requires dynamic lifetime or polymorphic indirection.
Watch exclusive ownership for the local-owner and owning-member examples. Then watch ownership transfer for moving a unique_ptr across a function boundary. Focus on which operation transfers responsibility and which uses the object without taking responsibility.
You would ordinarily construct a concrete object with std::make_unique, then move the owner when responsibility changes:
auto transport = std::make_unique<ConcreteTransport>();
engine.transport = std::move(transport);
// engine now owns it; transport is empty.
This assumes ConcreteTransport is a suitable implementation of Transport. The ownership point is the same as with UniqueFd: moving an owner changes who must perform cleanup; copying an owner would incorrectly duplicate that duty, so unique_ptr cannot be copied.
By contrast, a function that merely calls the transport should not take a unique_ptr:
void check_connection(const Transport& transport); // Borrow for this call.
void install_transport(std::unique_ptr<Transport> transport); // Take ownership.
The first function needs its argument to remain alive during the call but does not extend its lifetime. The second signature says the caller must hand over an owner. A raw pointer can also represent a non-owning, possibly absent object, but neither a reference nor a raw pointer keeps it alive.
Other reasons for unique indirection include an object that must outlive its creator while ownership moves between components, or a collection whose objects need stable addresses as the collection grows. For example, a std::vector<std::unique_ptr<Node>> can move its pointer elements during reallocation without moving the separately allocated Node objects. Erasing an owning pointer still destroys its node, so stable address does not mean unlimited lifetime.
Share ownership only when lifetimes genuinely overlap
Suppose an engine publishes one immutable snapshot to two asynchronous receivers. Either receiver may finish last, and each must be able to keep that same snapshot alive until its own work completes. This is a case for std::shared_ptr:
#include <cstdint>
#include <memory>
struct Snapshot {
std::uint64_t last_sequence;
};
auto producer = std::make_shared<const Snapshot>(Snapshot{42});
auto receiver_a = producer;
auto receiver_b = producer;
These are three distinct shared_ptr objects participating in one ownership group. Copying producer does not copy the Snapshot; it adds another owner of that same snapshot. When the last owning pointer releases it, the snapshot is destroyed. Making the snapshot const expresses that receivers may share its lifetime without being invited to change its contents.
This diagram provides a useful conceptual model of how copies coordinate:

Watch the talk’s explanation of the control block and its practical limits:
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
Olsen explains how copied shared_ptr objects cooperate, why independently wrapping the same raw address does not work, and what reference counting does—and does not—make safe across threads.
Watch the control block, then sharing safely. The latter range covers make_shared, the double-ownership mistake, and the distinction between safe ownership counting and access to the managed object. Finish with the choice between unique and shared ownership.
The control block is why this is correct:
auto first = std::make_shared<Snapshot>(Snapshot{42});
auto second = first; // Same ownership group.
But this is not a way to create shared ownership:
Snapshot* raw = new Snapshot{42};
std::shared_ptr<Snapshot> first{raw};
std::shared_ptr<Snapshot> second{raw}; // Wrong: independent control block.
Both pointers would believe they are the last responsible owner at different times, leading to invalid deletion. Create one owning shared_ptr and copy it. Likewise, never form a new owning smart pointer from first.get(); get() only borrows an address.
Shared ownership has a cost and a design consequence. Copies maintain a shared reference count, and destruction may occur in whichever component releases the final owner. In a latency-sensitive path, that can make both ownership traffic and the location of cleanup less predictable than unique or value ownership. Use it because independent consumers must extend the same object’s lifetime, not because many functions happen to read the object. If all readers finish while a known owner remains alive, borrowing from that owner may be enough.
Reference-count operations on different shared_ptr instances can safely manage their common ownership across threads. They do not make writes to the Snapshot thread-safe, nor do they make unsynchronized concurrent access to the same shared_ptr variable safe. Ownership synchronization and data synchronization answer different questions; later concurrency lessons will develop that distinction.
Observe an expirable shared object with weak_ptr
A diagnostic cache might remember the latest snapshot if it is still available, without requiring that snapshot to remain alive just for the cache. A raw pointer would be dangerous after the receivers finish. std::weak_ptr expresses non-owning observation of a shared ownership group:
std::weak_ptr<const Snapshot> latest = producer;
if (auto pinned = latest.lock()) {
// pinned is a shared_ptr: the snapshot stays alive during this scope.
inspect(*pinned);
} else {
// The snapshot has already been destroyed.
}
lock() attempts to acquire temporary shared ownership. It either returns a valid shared_ptr that protects the object for this use, or an empty one. Do not check for existence and then assume it will still exist later: obtain and retain the result of lock() for the whole access.
Watch the corresponding segment of Olsen’s talk:
Back to Basics: C++ Smart Pointers - David Olsen - CppCon 2022
The talk introduces std::weak_ptr as a way to find out whether a shared object still exists without keeping it alive merely by observing it.
Watch weak observation. Pay particular attention to why access goes through lock() and why a weak cache entry can remain after its object is gone.
Weak ownership is also important in graphs with back-references. Imagine a parent that keeps a child alive and a child that needs to find its parent:
struct Node {
std::shared_ptr<Node> child; // Owning relationship.
std::weak_ptr<Node> parent; // Non-owning back-reference.
};
auto parent = std::make_shared<Node>();
auto child = std::make_shared<Node>();
parent->child = child;
child->parent = parent;
child.reset();
parent.reset();
After the local child owner is reset, the parent still owns the child. Resetting the local parent owner then destroys the parent and releases its ownership of the child; the child can be destroyed too. If child->parent were instead a shared_ptr, the two objects would keep each other’s ownership counts above zero after both local owners disappeared.
That does not mean every parent–child relationship needs shared and weak pointers. If a parent exclusively owns its child and the child cannot outlive that parent, a unique_ptr child plus a carefully scoped non-owning back-reference may express the design more simply. weak_ptr is useful specifically when the observed object is managed by shared_ptr and may expire independently of the observer.
Choose edges, not a pointer type for the whole graph
An object graph rarely needs one ownership mechanism everywhere. For the example service, these edges have different meanings:
| Relationship | Suitable representation | Lifetime reason |
|---|---|---|
Engine holds its current FeedState |
Value member | State lives with the engine. |
| Engine holds a runtime-selected transport | std::unique_ptr<Transport> |
One engine owns a polymorphic object. |
| Two asynchronous receivers retain one immutable snapshot | Copies of std::shared_ptr<const Snapshot> |
Either receiver may be the last to finish. |
| Cache remembers a snapshot if it still exists | std::weak_ptr<const Snapshot> |
The cache must not prolong its lifetime. |
| Function examines a transport during a call | const Transport& |
Access is needed, but ownership does not change. |
A reliable selection process is to ask, in order:
- Can this be an ordinary object or member? Prefer a value when its desired lifetime follows its scope or containing object.
- If indirection is needed, is there one owner at a time? Use
unique_ptr, moving it when ownership changes. - Must independent holders keep the same object alive for potentially different durations? Use
shared_ptrand make that shared lifetime an explicit design decision. - Must a holder find an object without keeping it alive? If the object is shared-owned and may expire, use
weak_ptrandlock(). If its lifetime is already guaranteed for a short use, a reference or non-owning pointer may suffice.
The key question is not “How many places mention this object?” It is “Which places are allowed to determine when this object can be destroyed?”
Conclusion
Default to value ownership when one scope or containing object naturally governs lifetime. Use unique_ptr for one dynamically held owner, shared_ptr when independent owners must prolong one object’s lifetime, and weak_ptr when an observer of a shared object must tolerate its disappearance. Borrowing an object for a call does not itself justify shared ownership.
The next lesson will examine copy and move operations for resource-owning types. Today’s choices set up that question: copying a value, moving a unique owner, and copying a shared owner have deliberately different meanings.