Stop Calling delete: A Practical Guide to unique_ptr and shared_ptr in Modern C++
Raw delete calls are one exception away from a leak. Here's how to choose between unique_ptr, shared_ptr, and weak_ptr — with working code, the cycle trap, and a sanitizer check.
18 Dec 2025, 18:41 UTC

If your C++ code still contains raw delete calls outside of a destructor, you are one early return, one exception, or one refactoring away from a leak. The fix is not discipline — it is ownership semantics. Smart pointers encode who owns an object directly in the type system, so the compiler enforces cleanup instead of your memory.
The takeaway: default to std::unique_ptr, reach for std::shared_ptr only when ownership is genuinely shared, and use std::weak_ptr to observe shared objects without keeping them alive. Everything below assumes C++17 or later, which is available in GCC 7+, Clang 5+, and MSVC 2017+.
RAII: why smart pointers work at all
Smart pointers are an application of RAII (Resource Acquisition Is Initialization): an object's lifetime is tied to a scope, and its destructor releases the resource. When a unique_ptr goes out of scope — normally, via return, or because an exception unwound the stack — its destructor runs and deletes the managed object. That last point is the one raw pointers get wrong most often: exceptions skip delete entirely.
unique_ptr: exclusive ownership, zero overhead
std::unique_ptr owns exactly one object and cannot be copied — only moved. That move-only rule is the feature: it makes accidental double-ownership a compile error instead of a runtime double-free.
#include <memory>
#include <vector>
struct Connection {
void send(const char* msg);
~Connection(); // closes the socket
};
std::unique_ptr<Connection> open(const char* host) {
auto conn = std::make_unique<Connection>(/* host */);
// ... if anything here throws, conn is still cleaned up
return conn; // ownership moves to the caller
}
void worker() {
auto conn = open("example.internal");
conn->send("ping");
} // Connection destroyed here, even if send() threwPrefer std::make_unique<T>(...) over unique_ptr<T>(new T(...)): it is shorter and avoids a window where a leaked raw pointer is possible if an exception intervenes between allocation and pointer construction. A unique_ptr is typically the size of one raw pointer and compiles down to the same code as manual new/delete — you can confirm this by inspecting the generated assembly with g++ -O2 -S.
Because it is cheap and movable, unique_ptr works well in containers (std::vector<std::unique_ptr<Shape>> for polymorphic collections) and as a factory return type.
shared_ptr: shared ownership, real costs
std::shared_ptr keeps a reference count in a control block; the object dies when the last owner does. Use it when lifetime genuinely cannot be tied to a single scope — for example, a cached object handed out to multiple consumers that release it at unpredictable times.
auto config = std::make_shared<Config>(loadConfig());
readerA.start(config); // each consumer holds a copy
readerB.start(config); // Config outlives this function safelyThe costs are real, if usually small: the control block is an extra allocation (avoided by make_shared, which allocates object and control block together), and the count is updated with atomic operations, so copies are not free in hot loops. Copying a shared_ptr per iteration in a tight loop is a common, invisible slowdown — pass by reference or raw pointer when the callee does not need to extend lifetime.
The classic trap: reference cycles
Two shared_ptr objects pointing at each other never reach a count of zero. The canonical case is a parent/child or doubly linked structure:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // observing link: breaks the cycle
};Rule of thumb: ownership arrows point one way (shared_ptr), back-references are weak_ptr. A weak_ptr does not keep the object alive; call lock() to get a temporary shared_ptr, which returns empty if the object is gone — that emptiness check is your safe way to handle expired observers.
Trade-offs and when to skip smart pointers
Smart pointers manage heap ownership, not every pointer. Non-owning parameters and iterators should stay raw pointers or references — wrapping them in shared_ptr adds atomic traffic and implies ownership that does not exist. In latency-critical code (audio callbacks, trading hot paths), the allocation itself is often the problem, not the pointer; a pool allocator or stack storage may be the right answer. Custom deleters (std::unique_ptr<FILE, decltype(&fclose)>) extend the same RAII pattern to C resources like file handles.
Verify it yourself
Write a small program that creates, moves, and cycles the pointers above, compile with g++ -std=c++17 -fsanitize=address -g main.cpp, and run it. AddressSanitizer will report leaks (including a shared_ptr cycle, if you build one without weak_ptr) and double-frees at exit, so you can watch the fix work rather than trust it. You can also check __cplusplus >= 201703L to confirm your standard level. The goal is not to memorize the rules — it is to make the type system and the sanitizer enforce them for you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.