conceptModern C++~4 min readUpdated 2026-07-02#cpp#smart-pointers#unique-ptr#shared-ptr#ownership

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_ptr as 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_ptr can keep each other alive forever. Use weak_ptr for 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. Use make_shared, make_unique, or pass existing smart pointers.
  • get() is a borrow, not a transfer. Passing ptr.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, use std::unique_ptr<T[]>, not std::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_unique and make_shared. They avoid naked new, make ownership immediate, and reduce exception-safety holes.
  • Pass unique_ptr by value only to transfer. For a borrow, pass T&, const T&, or T*. For reseating an owner, pass std::unique_ptr<T>&.
  • Pass shared_ptr by value only to share lifetime. If the function only needs access, pass a reference to T instead 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, and make_shared. https://en.cppreference.com/w/cpp/memory/shared_ptr
  • cppreference - std::weak_ptr - non-owning observation of shared_ptr objects and cycle-breaking. https://en.cppreference.com/w/cpp/memory/weak_ptr
  • C++ working draft - [unique.ptr] - standard-library wording for unique_ptr. https://eel.is/c++draft/unique.ptr
  • C++ working draft - [util.smartptr.shared] - standard-library wording for shared_ptr. https://eel.is/c++draft/util.smartptr.shared
  • C++ working draft - [util.smartptr.weak] - standard-library wording for weak_ptr. https://eel.is/c++draft/util.smartptr.weak
  • C++ Core Guidelines - ownership rules such as preferring unique_ptr and representing ownership explicitly. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines