Use std::unique_ptr to Guarantee Single Release of Dynamically Allocated Resources in C++
Learn how to replace manual new and delete with std::unique_ptr so C++ resources are released exactly once, even when exceptions propagate. Includes compiler flags, sanitizer checks, and migration paths.
04 Apr 2026, 05:21 UTC

The Problem: Leaks When Control Leaves Early
Manual resource management with new and delete assumes execution follows a straight line from allocation to deallocation. In real code, exceptions, early returns, and break statements interrupt that path. When they do, the delete never runs and the resource leaks.
std::unique_ptr solves this by tying the resource's lifetime to a local variable's scope. When the variable goes out of scope—whether normally or during exception propagation—its destructor releases the owned resource exactly once.
Prerequisites
- A compiler with C++11 support or newer.
std::make_uniquerequires C++14; in C++11 usestd::unique_ptr<T>(new T(args...))instead. - The
<memory>header included in the translation unit. - Understanding that
unique_ptrmodels exclusive ownership: exactly one pointer owns the object at any time.
Procedure: Converting a Raw Allocation to Scoped Ownership
Step 1: Replace the raw allocation
Replace T* raw = new T(args); with a std::unique_ptr<T>. Prefer std::make_unique<T>(args...) when your compiler supports C++14 or later, because it avoids repeating the type name and is exception-safe during construction.
#include <memory>
struct Session {
Session(int id) { /* acquire resource */ }
~Session() { /* release resource */ }
void run() { /* may throw */ }
};
void handle(int id) {
// C++14 and later
std::unique_ptr<Session> s = std::make_unique<Session>(id);
s->run();
// s is destroyed here, even if run() throws.
}
In C++11, write std::unique_ptr<Session> s(new Session(id)); instead.
Step 2: Let scope determine lifetime
Do not call delete manually. The destructor runs automatically when the unique_ptr leaves scope. If you must release the resource before the end of the scope, call reset() with no arguments, or assign a new pointer to it.
Step 3: Transfer ownership explicitly
Because unique_ptr cannot be copied, use std::move to pass ownership into a function or container. After the move, the source pointer is null.
void take_ownership(std::unique_ptr<Session> s);
void handle(int id) {
auto s = std::make_unique<Session>(id);
take_ownership(std::move(s));
// s is now null; the callee owns the resource.
}
Expected Checks: Prove the Resource Is Released Exactly Once
Compile-time check
Compile with strict warnings. The compiler will reject accidental copies at compile time because the copy constructor is deleted.
Run in your shell or terminal (standard user permissions are sufficient for local compilation):
g++ -std=c++17 -Wall -Wextra -c main.cpp -o main.o
Expected result: no errors about deleted copy constructors or mismatched deletes.
Runtime check with AddressSanitizer
AddressSanitizer (ASan) instruments the binary to detect leaks, double-frees, and use-after-free errors at runtime.
g++ -std=c++17 -Wall -Wextra -fsanitize=address -g main.cpp -o app
./app
Expected result: the program exits with status 0 and ASan prints no leak or error report. If a leak exists, ASan prints a stack trace showing where the allocation happened.
Risk: Sanitizers add significant runtime and memory overhead. Do not enable them in production builds.
Exception-safety unit test
Write a test that forces an exception inside the owning scope and asserts that the destructor ran.
#include <cassert>
static bool destroyed = false;
struct Tracked {
~Tracked() { destroyed = true; }
};
void test_exception_safety() {
destroyed = false;
try {
std::unique_ptr<Tracked> t = std::make_unique<Tracked>();
throw std::runtime_error("boom");
} catch (const std::runtime_error&) {
// Expected
}
assert(destroyed && "destructor must run during unwinding");
}
Recovery Options and Limitations
When shared ownership is required
unique_ptr cannot be copied. If multiple parts of the program genuinely need shared access and no single owner exists, migrate to std::shared_ptr<T>. Use reference counting only when necessary; it adds atomic overhead and can create cycles that leak.
Custom deleters
For resources that are not released with delete—such as FILE* handles or sockets—supply a custom deleter to the constructor.
#include <cstdio>
void use_file() {
std::unique_ptr<FILE, int(*)(FILE*)> f(fopen("data.txt", "r"), [](FILE* p){
if (p) fclose(p);
});
// fclose runs automatically.
}
Array allocations
Use std::unique_ptr<T[]> for arrays so the destructor calls delete[]. In C++14 and later, prefer std::make_unique<T[]>(n). The scalar form std::unique_ptr<T> calls scalar delete, which is undefined behavior for arrays.
| Allocation form | Correct unique_ptr type | Delete operator invoked |
|---|---|---|
| Single object | std::unique_ptr<T> | delete |
| Array of objects | std::unique_ptr<T[]> | delete[] |
Limitations
unique_ptris a lightweight wrapper with no built-in thread safety. Sharing a raw pointer obtained viaget()across threads is unsafe unless externally synchronized.- It is not a garbage collector. If you create reference cycles with
shared_ptr, resources will still leak. - Custom deleters increase the size of the
unique_ptrobject itself when they capture state, unlike the default deleter which is stateless.
Practical Verification Checklist
- Search the codebase for remaining
new/deletepairs in the modified module. - Compile with
-Wall -Wextra -std=c++17and confirm zero warnings. - Run under AddressSanitizer or Valgrind Memcheck and confirm no leak reports.
- Force an exception in the owning scope and verify the destructor executes.
- Confirm that no code attempts to copy the
unique_ptr(the compiler will catch this).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.