C++ smart pointers are standard library types that manage dynamically allocated objects using RAII (Resource Acquisition Is Initialization). Instead of manually pairing new with delete, you express ownership in the type system, and cleanup happens automatically when the owner goes out of scope.
Prerequisites and scope
- Assumes C++14+ (for
std::make_unique). - Examples use
<memory>(and occasionally<vector>,<cstdio>). - This post focuses on ownership/lifetime management. Raw pointers (
T*) are still useful for non-owning references, array traversal, and C APIs.
Cheat sheet (anchor)
unique_ptr: owns (exclusive), no copies (moves only), use for a single clear owner.shared_ptr: owns (shared), copies share ownership, use only for shared lifetime.weak_ptr: non-owning observer ofshared_ptr, break cycles / observe without extending lifetime.
Why smart pointers exist
Raw pointers (T*) are not inherently “bad”—they’re great for non-owning references and interoperability. The problem is owning raw pointers: it’s easy to leak memory, double-delete, or forget cleanup when exceptions occur.
Smart pointers address this by:
- Making ownership explicit
- Automatically releasing resources in destructors
- Working correctly with exceptions (stack unwinding)
Rule of thumb: use raw pointers for non-owning access, smart pointers for owning relationships.
std::unique_ptr: exclusive ownership
std::unique_ptr<T> represents sole ownership of a T. There is exactly one unique_ptr responsible for deleting the object.
Key properties:
- Not copyable, but movable
- Very low overhead (often just a pointer, but a custom deleter can increase size)
- Supports custom deleters
- Ideal default choice for ownership
Creating a unique_ptr
Prefer std::make_unique (C++14+):
#include <memory>
auto p = std::make_unique<int>(42);
For a user-defined type:
struct Widget {
void run();
};
auto w = std::make_unique<Widget>();
w->run();
Returning and moving a unique_ptr (move or elision)
Because it can’t be copied, transferring ownership uses move semantics. When you return a unique_ptr, it will be moved or elided (copy elision is allowed; a move is fine too).
std::unique_ptr<Widget> makeWidget() {
return std::make_unique<Widget>(); // moved or elided
}
auto a = std::make_unique<Widget>();
auto b = std::move(a); // ownership transfers to b
// a is now nullptr
Reminder: after std::move(a), treat a as moved-from; for unique_ptr that means it’s typically nullptr. Avoid use-after-move.
Using unique_ptr in containers
unique_ptr works well in containers because it’s movable:
#include <vector>
#include <memory>
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());
Array ownership: prefer containers, but know the tool
If you need a dynamic array, prefer std::vector<T> (size-aware, rich API) or std::array<T, N> (fixed size).
If you truly need an owning pointer to a dynamically allocated array, use std::unique_ptr<T[]>:
#include <memory>
auto arr = std::make_unique<int[]>(10); // value-initialized elements
arr[0] = 123;
Notes:
std::unique_ptr<T>andstd::unique_ptr<T[]>are different specializations.make_unique<T[]>(n)allocates an array; you access elements withoperator[].- Don’t use
std::unique_ptr<T>to ownnew T[n].
Custom deleters (and why they matter)
Custom deleters are useful for resources not freed by delete (e.g., FILE*, OS handles, C allocations). Always handle acquisition failure (e.g., fopen can return nullptr).
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(FILE* f) const noexcept {
if (f) std::fclose(f);
}
};
using FilePtr = std::unique_ptr<FILE, FileCloser>;
FilePtr openFile(const char* path) {
// If fopen fails, returns nullptr; unique_ptr will hold nullptr safely.
return FilePtr(std::fopen(path, "r"));
}
Deleter details to be aware of:
- The deleter type is part of the
unique_ptrtype. A stateless functor deleter often has no size overhead (can be empty-base optimized), while a function pointer deleter typically adds storage. - Changing deleter types can affect ABI and object size—important at library boundaries.
Transition: when unique ownership isn’t enough
Start with unique_ptr whenever there’s a clear owner. If you discover you truly need multiple parts of the program to co-own the same object (shared lifetime), that’s when you “upgrade” to shared_ptr.
std::shared_ptr: shared ownership (reference counting)
std::shared_ptr<T> represents shared ownership: multiple shared_ptrs can point to the same object, and the object is deleted when the last owning shared_ptr is destroyed.
Internally, shared_ptr uses a control block that stores:
- Strong reference count (owners)
- Weak reference count (observers)
- Deleter/allocator information
Takeaway: the object is destroyed when strong_count == 0. The control block is destroyed only when both strong_count == 0 and weak_count == 0.
Creating a shared_ptr
Prefer std::make_shared:
#include <memory>
auto sp = std::make_shared<std::string>("hello");
Benefits of make_shared:
- Typically one allocation for control block + object (better locality)
- Exception safety
Tradeoff to know:
- Because the control block and object are often allocated together, the object’s memory may be kept alive until the control block is freed (e.g., while
weak_ptrs still exist), even though the object has been destroyed. This can matter for very large objects or memory-sensitive code. - Custom allocator / allocation strategies may also push you away from
make_sharedin some designs.
Copying increments the count
auto a = std::make_shared<int>(7);
auto b = a; // shares ownership; strong count increases
When a and b both go out of scope, the managed int is deleted.
Thread-safety note
- Operations that adjust the reference counts are generally implemented to be thread-safe (often via atomics; exact details are implementation-defined).
- The pointed-to object itself is not automatically synchronized. If multiple threads access/modify
*sp, you still need your own synchronization.
When shared_ptr is a good fit
Use shared_ptr when you truly have shared lifetime:
- Objects owned by multiple subsystems without a clear single owner
- Graph-like structures (but watch for cycles)
- Caching scenarios where values can outlive the caller
If there is a clear owner, prefer unique_ptr and pass raw pointers/references for non-owning access.
Common pitfall: constructing multiple shared_ptr from the same raw pointer
This is a classic bug:
T* raw = new T;
std::shared_ptr<T> a(raw);
std::shared_ptr<T> b(raw); // BUG: two control blocks => double delete
Instead, create one shared_ptr and copy it:
auto a = std::make_shared<T>();
auto b = a; // OK
If you must start from an existing raw pointer, do it once, with a clear deleter, and distribute copies:
T* raw = /* obtained from a C API or legacy code */;
// Create exactly one owner control block for this raw pointer:
auto a = std::shared_ptr<T>(raw, [](T* p){ delete p; });
// Now share ownership by copying:
auto b = a;
Warning: only do this if no other owner will delete raw. Mixing ownership models (some code calls delete, other code uses shared_ptr) is a recipe for double-free.
enable_shared_from_this
If an object needs to create a shared_ptr to itself (e.g., handing out shared ownership from a member function), use std::enable_shared_from_this.
#include <memory>
struct Node : std::enable_shared_from_this<Node> {
std::shared_ptr<Node> getptr() {
return shared_from_this();
}
};
auto n = std::make_shared<Node>();
auto n2 = n->getptr(); // shares ownership correctly
Key precondition:
- The object must already be owned by a
shared_ptr(e.g., created viamake_sharedor otherwise placed undershared_ptrownership) before callingshared_from_this(). Otherwise it throwsstd::bad_weak_ptr(or can be undefined behavior in some misuses).
Do not create std::shared_ptr(this) inside a member function—this creates a second control block.
Transition: shared ownership creates a new problem—cycles
Once you start sharing ownership, it’s easy to create reference cycles (especially in graphs, parent/child relations, and observer patterns). That’s where weak_ptr comes in.
std::weak_ptr: non-owning observer of shared_ptr
std::weak_ptr<T> is a companion to shared_ptr. It observes an object managed by shared_ptr without increasing the strong reference count.
Why this matters:
- It helps break reference cycles
- It lets you safely check whether an object still exists
Takeaway: weak_ptr does not keep the object alive. Use lock() to get a temporary owning shared_ptr while you access the object.
Creating and using weak_ptr
std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp; // does not extend lifetime
sp.reset(); // object may be destroyed here
if (auto locked = wp.lock()) {
// object is still alive; locked is a shared_ptr
locked->run();
} else {
// object expired
}
Important APIs:
wp.lock()gives ashared_ptrif alive, else an emptyshared_ptrwp.expired()tells you if the object is gone, but in multi-threaded code it can race; prefer the check-and-use pattern vialock().
Breaking cycles with weak_ptr (real-world pattern)
Two objects that hold shared_ptr to each other form a cycle, so they are never destroyed because strong counts never reach zero.
Common real-world examples:
- Parent/child relationships: parent owns children; child references parent (should be weak)
- Observer pattern: subject owns observers; observers reference subject (often weak)
Cycle bug:
struct B;
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
// Leak via cycle: strong counts never reach zero
Fix by making one side weak_ptr:
struct B;
struct A { std::shared_ptr<B> b; };
struct B { std::weak_ptr<A> a; };
Takeaway: make at least one direction of a bidirectional relationship non-owning to allow cleanup.
Choosing the right smart pointer
A practical decision guide:
- Use
unique_ptrby default for ownership. - Use
shared_ptronly when you need shared lifetime. - Use
weak_ptrto observe ashared_ptrwithout owning (especially to break cycles).
Decision table
| Situation | Recommended type | Notes |
|---|---|---|
| One clear owner | std::unique_ptr<T> | Cheapest owning option; move to transfer ownership |
| Many parts must co-own lifetime | std::shared_ptr<T> | More overhead; watch for cycles |
| Need a back-pointer / observer without extending lifetime | std::weak_ptr<T> | Use lock() to access safely |
| Non-owning access only | T*, T&, std::span<T> | Don’t store ownership in raw pointers |
Passing smart pointers to functions (value vs reference)
A good rule is: pass ownership types by value only when the function participates in ownership.
- Function does not take ownership: prefer
T&,const T&, orT*(non-owning) - Function takes ownership exclusively: take
std::unique_ptr<T>by value - Function may share/store ownership: take
std::shared_ptr<T>by value - Function only needs to observe a shared object: take
std::weak_ptr<T>(or sometimesconst std::shared_ptr<T>&if you just need to read and don’t want to increment counts)
Examples:
void use(const Widget& w); // no ownership
void take(std::unique_ptr<Widget> w); // takes ownership
void store(std::shared_ptr<Widget> w); // shares/stores ownership
void observe(std::weak_ptr<Widget> w); // observes without owning
Common anti-patterns
- “Just use
shared_ptreverywhere”: increases overhead and can hide unclear ownership design. - Aliasing/ownership confusion: multiple independent owners of the same raw pointer (double-delete territory).
- Using
newwith smart pointers by default: prefermake_unique/make_sharedfor exception safety and clarity (usenewonly when you have a specific reason).
Performance and correctness notes
unique_ptris usually as cheap as a raw pointer, but a non-empty deleter can increase its size.shared_ptrhas overhead: reference counting (commonly atomic for thread-safety), control block management, and potential contention.- Prefer
make_unique/make_sharedfor safety and efficiency, but remember themake_sharedlifetime/allocation tradeoff. - Avoid
shared_ptrcycles; useweak_ptrto break them. - Don’t return or store owning raw pointers; make ownership explicit.
Summary
Smart pointers are about expressing ownership:
std::unique_ptr: one owner, movable, best defaultstd::shared_ptr: multiple owners, reference counted, use when lifetimes are genuinely sharedstd::weak_ptr: non-owning observer to safely reference shared objects and break cycles
A final std::move reminder: moving a unique_ptr transfers ownership and leaves the source empty (typically nullptr). Treat moved-from pointers as invalid for use except for checks/reset.
References
- cppreference:
std::unique_ptr,std::shared_ptr,std::weak_ptr,std::enable_shared_from_this - C++ Core Guidelines: R.20–R.24 (smart pointer and ownership guidance): https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-smart