Smart pointers: unique_ptr, shared_ptr, ownership
A smart pointer is not "a pointer that cannot be wrong." It is an object that stores a
pointer and runs an ownership policy in its destructor. std::unique_ptr<T> means exactly
one owner will delete the object. std::shared_ptr<T> means a control block counts shared
owners and deletes the object when the last one leaves. std::weak_ptr<T> observes a
shared_ptr-managed object without extending its lifetime. The point is not to replace
all pointers; it is to make owning pointers visible.
The reset: raw pointers and references are fine for borrowing. Smart pointers are for ownership.
How it really works
std::unique_ptr<T> is the default owning pointer. It is move-only, so passing it by value
means transfer. Destroying it calls its deleter on the stored pointer. In normal code, that
means delete p; with arrays it means delete[]; with a custom deleter it can mean
fclose, close, munmap, or a C-library destroy function. A unique pointer usually has
the same size as a raw pointer when the deleter is stateless.
std::shared_ptr<T> is a different tool. It stores or points at a control block containing
the strong owner count, weak observer count, deleter, and often allocator state. Copying a
shared_ptr increments the strong count. Destroying one decrements it. When the strong
count reaches zero, the managed object is destroyed. When both strong and weak counts are
gone, the control block can be deallocated. Those count updates have overhead and are
thread-safe with respect to distinct shared_ptr objects sharing the same control block.
std::weak_ptr<T> exists because shared ownership can form cycles. If A owns B through
a shared_ptr and B owns A through another shared_ptr, both counts can stay non-zero
after the outside world drops the graph. A weak pointer breaks that ownership edge. You call
lock() to attempt temporary ownership; it returns an empty shared_ptr if the object is
already gone.
| Situation | Type to reach for | Why |
|---|---|---|
| Function borrows a required object | T& or const T& |
no ownership transfer, cannot be null |
| Function borrows an optional object | T* |
nullable borrow, no deletion implied |
| Function creates and returns ownership | std::unique_ptr<T> or a value |
transfer is explicit |
| Object has one owner but must move | std::unique_ptr<T> |
copy is disabled, move is ownership transfer |
| Lifetime is genuinely shared | std::shared_ptr<T> |
last owner destroys |
| Back-pointer or cache observation | std::weak_ptr<T> |
observes without keeping alive |
Executable artifact: explicit ownership transfer
The runnable demo lives in
examples/modern-cpp/smart-pointers-unique-ptr-shared-ptr-ownership/. It compares a C
owner-transfer protocol using Packet ** with the C++ version using std::unique_ptr,
std::shared_ptr, and std::weak_ptr.
cd examples/modern-cpp/smart-pointers-unique-ptr-shared-ptr-ownership
./run.sh
The C side has to document transfer by convention:
static Packet *packet_take(Packet **slot) {
Packet *packet = *slot;
*slot = NULL;
return packet;
}
The C++ side makes transfer a compile-time fact:
auto owner = make_packet("unique");
auto next_owner = std::move(owner);
std::cout << (owner ? "not null" : "null") << "\n";
std::cout << next_owner->payload << "\n";
std::move does not move by itself; it casts owner to an xvalue so the move constructor
or move assignment can be selected. For std::unique_ptr, that move transfers the stored
pointer and leaves the source empty. The C program can forget to null the old owner; the
C++ type does that as part of its move operation.
The shared part of the demo uses a std::weak_ptr observer:
std::weak_ptr<CacheEntry> observer;
{
auto shared = std::make_shared<CacheEntry>("cache-line");
observer = shared;
auto second_handle = shared;
}
std::cout << observer.expired() << "\n";
After both strong owners leave the scope, the CacheEntry is destroyed and the weak
observer reports expiration instead of dangling.
Failure modes & trade-offs
- Do not use
shared_ptras the default. It makes ownership less local, adds reference count traffic, and can hide architectural uncertainty. - Cycles leak. Two objects that own each other through
shared_ptrcan keep each other alive forever. Useweak_ptrfor parent links, observers, caches, and back edges. shared_ptr<T>(raw)twice is a double-delete bug. Two independent control blocks now believe they own the same raw pointer. Usemake_shared,make_unique, or pass existing smart pointers.get()is a borrow, not a transfer. Passingptr.get()to code that stores or deletes the pointer breaks the ownership model.- Custom deleters are part of the type for
unique_ptr. That can affect APIs and object size. It is worth it when wrapping C handles. - Arrays need the right owner. Use
std::vector<T>for most dynamic arrays. If you truly need a smart pointer to an array, usestd::unique_ptr<T[]>, notstd::unique_ptr<T>.
In practice
- Return values before heap ownership. If a type is cheap or movable, return it by value and let move/copy elision work.
- Use
make_uniqueandmake_shared. They avoid nakednew, make ownership immediate, and reduce exception-safety holes. - Pass
unique_ptrby value only to transfer. For a borrow, passT&,const T&, orT*. For reseating an owner, passstd::unique_ptr<T>&. - Pass
shared_ptrby value only to share lifetime. If the function only needs access, pass a reference toTinstead of incrementing a reference count. - Model ownership in member fields. A raw pointer data member should make reviewers ask: "is this a borrow, an optional borrow, an index into another owner, or a bug?"
Connects to: RAII: the idea that changes everything · Move semantics and value categories · What a pointer really is · Pointers to pointers · Memory leaks & ownership discipline · Use-after-free & double-free · Arrays and array-to-pointer decay
Sources
- cppreference -
std::unique_ptr- move-only exclusive ownership, deleters, array specialization, and member operations. https://en.cppreference.com/w/cpp/memory/unique_ptr - cppreference -
std::shared_ptr- shared ownership, control blocks, reference counts, andmake_shared. https://en.cppreference.com/w/cpp/memory/shared_ptr - cppreference -
std::weak_ptr- non-owning observation ofshared_ptrobjects and cycle-breaking. https://en.cppreference.com/w/cpp/memory/weak_ptr - C++ working draft -
[unique.ptr]- standard-library wording forunique_ptr. https://eel.is/c++draft/unique.ptr - C++ working draft -
[util.smartptr.shared]- standard-library wording forshared_ptr. https://eel.is/c++draft/util.smartptr.shared - C++ working draft -
[util.smartptr.weak]- standard-library wording forweak_ptr. https://eel.is/c++draft/util.smartptr.weak - C++ Core Guidelines - ownership rules such as preferring
unique_ptrand representing ownership explicitly. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines