Smart Pointers#
Smart pointers, introduced in C++11, provide automatic memory management through
RAII (Resource Acquisition Is Initialization). They ensure that dynamically
allocated memory is properly deallocated when no longer needed, preventing memory
leaks and dangling pointers. Unlike raw pointers, smart pointers automatically
manage the lifetime of the objects they point to, calling the appropriate
destructor or deleter when the pointer goes out of scope. The three main smart
pointer types are unique_ptr for exclusive ownership, shared_ptr for
shared ownership with reference counting, and weak_ptr for non-owning
references that don’t prevent deallocation.
std::unique_ptr: Exclusive Ownership#
- Source:
std::unique_ptr represents exclusive ownership of a dynamically allocated
object. Only one unique_ptr can own a given object at a time, enforcing
a clear ownership model that prevents accidental sharing. When the unique_ptr
is destroyed, goes out of scope, or is reset, the owned object is automatically
deleted. Because ownership is exclusive, unique_ptr cannot be copied—only
moved—which transfers ownership from one pointer to another. This makes
unique_ptr ideal for factory functions, RAII wrappers, and any situation
where a single owner is responsible for an object’s lifetime. It has zero
overhead compared to raw pointers when using the default deleter.
#include <memory>
#include <iostream>
int main() {
auto ptr = std::make_unique<int>(42);
std::cout << *ptr << "\n"; // Output: 42
// Transfer ownership
auto ptr2 = std::move(ptr);
// ptr is now nullptr
// Array support
auto arr = std::make_unique<int[]>(5);
arr[0] = 10;
}
std::weak_ptr: Non-Owning Observer#
- Source:
std::weak_ptr holds a non-owning reference to an object managed by
shared_ptr. Unlike shared_ptr, it doesn’t contribute to the reference
count, so it doesn’t prevent the object from being deleted. This makes
weak_ptr essential for breaking circular references that would otherwise
cause memory leaks. It’s also useful for implementing caches, observer patterns,
and any situation where you need to check if an object still exists without
extending its lifetime. To access the object, call lock() which returns
a shared_ptr—if the object still exists, you get a valid shared_ptr
that temporarily extends the object’s lifetime; if it’s been deleted, you
get an empty shared_ptr. The expired() method provides a quick check
without creating a shared_ptr.
#include <memory>
#include <iostream>
int main() {
std::weak_ptr<int> weak;
{
auto shared = std::make_shared<int>(42);
weak = shared;
if (auto locked = weak.lock()) {
std::cout << *locked << "\n"; // 42
}
} // shared destroyed
std::cout << "Expired: " << weak.expired() << "\n"; // 1 (true)
}
Custom Deleters#
- Source:
Smart pointers can use custom deleters for resources that require special
cleanup beyond simple delete. This is essential when working with C library
resources (file handles, sockets, database connections), memory from custom
allocators, or any resource that needs specific cleanup logic. For unique_ptr,
the deleter type is part of the pointer type, which means different deleters
create different types. For shared_ptr, the deleter is type-erased and
stored in the control block, so different deleters don’t affect the pointer
type—this provides more flexibility but adds slight overhead.
#include <cstdio>
#include <memory>
int main() {
// unique_ptr with custom deleter (deleter is part of type)
auto file = std::unique_ptr<FILE, int(*)(FILE*)>(
fopen("test.txt", "w"), fclose);
if (file) {
fprintf(file.get(), "Hello\n");
} // fclose called automatically
// shared_ptr with custom deleter (type-erased)
std::shared_ptr<FILE> file2(fopen("test2.txt", "w"), fclose);
}
Lambda as deleter:
auto deleter = [](int* p) {
std::cout << "Deleting " << *p << "\n";
delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
std::make_unique_for_overwrite (C++20)#
C++20 added std::make_unique_for_overwrite which creates a unique_ptr
with default-initialized (not value-initialized) memory. For scalar types and
arrays of scalars, this means the memory is left uninitialized rather than
being zeroed. This optimization is useful when you plan to immediately overwrite
all the memory anyway, such as when reading data from a file or network into
a buffer. Avoiding unnecessary zero-initialization can provide measurable
performance improvements for large allocations.
#include <memory>
int main() {
// Value-initialized (zeroed): slower
auto arr1 = std::make_unique<int[]>(1000);
// Default-initialized (uninitialized): faster
auto arr2 = std::make_unique_for_overwrite<int[]>(1000);
// Must initialize before reading!
}
out_ptr and inout_ptr (C++23)#
C++23 added std::out_ptr and std::inout_ptr adaptors for interfacing
smart pointers with C APIs that return pointers through output parameters.
These adaptors eliminate the error-prone manual pattern of releasing the smart
pointer, calling the C function, and then resetting the smart pointer with
the result. out_ptr is for functions that only output a pointer (the smart
pointer should be empty beforehand), while inout_ptr is for functions that
may release an existing resource before outputting a new one.
#include <memory>
// C API that allocates and returns through output parameter
extern "C" int create_resource(void** out);
extern "C" void destroy_resource(void* p);
int main() {
std::unique_ptr<void, decltype(&destroy_resource)> ptr(nullptr, destroy_resource);
// C++23: out_ptr handles release and reset automatically
create_resource(std::out_ptr(ptr));
}
Common Pitfalls#
Smart pointers eliminate many memory management bugs, but they introduce their own set of pitfalls that can cause crashes, memory leaks, or undefined behavior. Understanding these common mistakes helps you use smart pointers correctly.
Creating shared_ptr from raw pointer multiple times:
This is one of the most dangerous mistakes. When you create a shared_ptr
from a raw pointer, it creates a new control block with a reference count of 1.
If you create another shared_ptr from the same raw pointer, it creates a
completely separate control block, also with count 1. When both shared_ptr
instances are destroyed, each thinks it’s the last owner and calls delete,
resulting in a double-free crash. Always create shared_ptr using
make_shared or by copying/moving existing shared_ptr instances.
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // BUG: double delete!
// Correct approach:
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // Shares ownership correctly
Circular references with shared_ptr:
When two or more objects hold shared_ptr references to each other, they
create a cycle that prevents either from being deleted. Each object’s reference
count never reaches zero because the other object holds a reference. This is
a memory leak that persists until the program ends. The solution is to use
weak_ptr for at least one direction of the relationship, breaking the
cycle. Common scenarios include parent-child relationships (parent owns child
with shared_ptr, child references parent with weak_ptr), doubly-linked
lists, and graph structures.
struct Node {
std::shared_ptr<Node> next; // Creates cycle, memory leak!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // Cycle: neither a nor b will ever be deleted
// Fix: use weak_ptr for one direction
struct FixedNode {
std::shared_ptr<FixedNode> next;
std::weak_ptr<FixedNode> prev; // Breaks the cycle
};
Calling shared_from_this before shared_ptr exists:
The shared_from_this() method only works after at least one shared_ptr
to the object has been created. Internally, enable_shared_from_this stores
a weak_ptr that is initialized when the first shared_ptr is created.
If you call shared_from_this() in the constructor (before any shared_ptr
exists) or on a stack-allocated object (which should never be managed by
shared_ptr), it throws std::bad_weak_ptr. Always ensure the object
is managed by shared_ptr before calling shared_from_this().
class Widget : public std::enable_shared_from_this<Widget> {
public:
Widget() {
// BUG: no shared_ptr owns this yet
// auto self = shared_from_this(); // throws bad_weak_ptr
}
void init() {
// OK: called after make_shared creates the shared_ptr
auto self = shared_from_this();
}
};
// Correct usage:
auto w = std::make_shared<Widget>();
w->init(); // Now shared_from_this() works
Storing this in a shared_ptr:
Never create a shared_ptr directly from this. This creates a new
ownership group that doesn’t know about any existing shared_ptr owners,
leading to double deletion. Use enable_shared_from_this instead.
class Bad {
public:
std::shared_ptr<Bad> get_ptr() {
return std::shared_ptr<Bad>(this); // BUG: double delete!
}
};
Smart Pointer Comparison#
The following table summarizes when to use each smart pointer type. Choose
the simplest pointer that meets your ownership requirements—prefer unique_ptr
for its zero overhead and clear ownership semantics, use shared_ptr only
when ownership truly needs to be shared, and use weak_ptr to observe
without owning.
Type |
Ownership |
Use Case |
|---|---|---|
|
Exclusive |
Single owner, factory functions, RAII wrappers |
|
Shared |
Multiple owners, shared resources, caches |
|
None |
Breaking cycles, observers, caches with expiration |