unique_ptr vs shared_ptr: Preventing Double Frees and Dangling Pointers in C++
Decide when to use std::unique_ptr and std::shared_ptr to eliminate double frees and dangling pointers, and why std::make_shared is safer than the constructor.
03 Dec 2025, 07:40 UTC

The crash you can’t reproduce in production
You ship a resource manager that hands out raw pointers. It works in testing. Weeks later, a customer reports a segfault during shutdown. You trace it to a double free: two subsystems both called delete on the same object because ownership was never clearly defined. Or the pointer was still in use after the owner released it—a dangling pointer. Manual new/delete makes these bugs easy to write and hard to find.
The thesis is simple: adopt RAII-based ownership with smart pointers. Make std::unique_ptr your default, reach for std::shared_ptr only when ownership is genuinely shared, and use std::weak_ptr to break reference cycles.
Why raw ownership breaks down
Raw pointers carry no ownership semantics. They do not know whether they own the object they point to, whether another pointer aliases the same memory, or whether the lifetime has already ended. In complex lifecycles—objects passed between threads, stored in containers, returned from factories—these gaps produce two classic defects:
- Double free: two owners both call
deleteon the same allocation, corrupting the heap. - Dangling pointer: a pointer outlives the object it references, so reads and writes hit freed memory.
Smart pointers encode ownership in the type system, so the compiler and runtime enforce correct cleanup.
unique_ptr: exclusive ownership with zero overhead
std::unique_ptr owns its object exclusively. It cannot be copied, only moved. When the unique_ptr goes out of scope, it destroys the managed object automatically. Because it compiles down to the same machine code as a raw pointer plus a deleter call, there is no runtime cost compared to manual delete.
Use it for exclusive resources: file handles, database connections, worker objects, or anything with a single clear owner.
Worked example: a transferable resource manager
The following snippet shows a minimal manager that takes ownership of a resource and transfers it to another owner. The key line is std::move, which converts the source unique_ptr into an rvalue so the destination can steal the pointer.
#include <memory>
#include <utility>
class Resource {
public:
void use() const {}
};
class ResourceManager {
std::unique_ptr<Resource> resource_;
public:
explicit ResourceManager(std::unique_ptr<Resource> r)
: resource_(std::move(r)) {}
// Transfers ownership out; manager becomes empty.
std::unique_ptr<Resource> release() {
return std::move(resource_);
}
};
int main() {
auto res = std::make_unique<Resource>();
ResourceManager mgr(std::move(res));
auto extracted = mgr.release();
extracted->use();
// res is now null; extracted owns the Resource.
return 0;
}
Where to run it: any POSIX shell or Windows terminal with a C++11-compliant compiler. No elevated permissions are required.
Compile and check:
g++ -std=c++17 -Wall -Wextra -o resource_manager resource_manager.cpp
./resource_manager
Expected check: the program compiles without warnings and exits with status 0. No double free or segmentation fault occurs because only one unique_ptr owns the Resource at any time.
Risk: after std::move(res), the source pointer res is null. Dereferencing it without a null check is undefined behavior. Always verify with if (ptr) before use.
shared_ptr: when ownership must be shared
Sometimes multiple parts of a program need to keep an object alive. std::shared_ptr maintains a reference count. Each copy increments the count; each destruction decrements it. When the count reaches zero, the object is deleted.
The trade-off is cost. The reference count is typically an atomic integer, so incrementing and decrementing it involves atomic operations that are slower than plain pointer copies. Under heavy contention, this overhead becomes measurable.
Do not construct shared_ptr from the same raw pointer twice
A common and dangerous mistake is creating two independent shared_ptr objects from one raw pointer:
auto* raw = new Resource;
std::shared_ptr<Resource> a(raw);
std::shared_ptr<Resource> b(raw); // BUG: two control blocks
Each shared_ptr thinks it is the sole owner of the control block. When both go out of scope, the object is deleted twice. Always use std::make_shared, which allocates the object and control block together and guarantees a single owner:
auto a = std::make_shared<Resource>();
auto b = a; // safe: shares the same control block
Breaking cycles with weak_ptr
Smart pointers do not magically fix every leak. If two objects hold shared_ptrs to each other, their reference counts never drop to zero. The cycle keeps both alive forever.
std::weak_ptr observes a shared_ptr without owning it. It does not affect the reference count. To access the object, call lock(), which returns a shared_ptr if the object still exists, or an empty pointer if it has been destroyed. Use weak_ptr for back-references, caches, and observer relationships.
Limitations and practical verification
Smart pointers manage lifetime, not logic. They cannot detect cycles unless you deliberately break them with weak_ptr. They also do not help when raw pointers escape into C APIs or are cached globally without bounds.
To verify your changes:
- Compile with warnings enabled:
g++ -std=c++17 -Wall -Wextra -fsanitize=address -g. - Run under Valgrind or AddressSanitizer to confirm no leaks or invalid frees.
- Unit-test ownership transfers: after
std::move, assert the source is null and the destination is valid.
Note that std::auto_ptr is removed in modern C++ standards; do not use it.
Actionable checklist
- Default to
std::unique_ptrfor exclusive ownership. - Use
std::make_unique(C++14) orstd::unique_ptr<T>(new T)to construct it. - Switch to
std::shared_ptronly when multiple owners truly need shared lifetime. - Always create
shared_ptrviastd::make_shared, never from a raw pointer more than once. - Break reference cycles with
std::weak_ptr. - Run a leak checker before merging ownership changes.
Start with the compiler. If a design forces you to copy a unique_ptr, that is a signal the ownership model needs rethinking—not a reason to reach for shared_ptr.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.