Exclusive Ownership: std::unique_ptr, make_unique, and Custom Deleters
Master exclusive resource ownership with std::unique_ptr: zero runtime overhead, move-only semantics, custom deleter function pointers, and std::make_unique.
By the end, you’ll be able to…
- Enforce single-owner resource semantics using std::unique_ptr and analyze why copy operations are deleted
- Implement stateless and stateful custom deleters for C API handles (fclose, free, custom destroyers)
- Explain why std::make_unique guarantees exception safety compared to direct new expressions
01The Architecture of std::unique_ptr
In modern C++, raw pointers should never denote ownership. A raw pointer should be used only for non-owning observation. Whenever an object or function owns a dynamically allocated heap resource, ownership must be modeled using std::unique_ptr<T>.
std::unique_ptr represents exclusive ownership: exactly one pointer owns the managed object at any given time. Consequently, std::unique_ptr explicitly deletes its copy constructor and copy assignment operator. It cannot be copied; it can only be moved.
Crucially, std::unique_ptr with a default deleter introduces ZERO runtime memory or performance overhead compared to a raw pointer. sizeof(std::unique_ptr<T>) is exactly 8 bytes (the size of a single raw pointer), and its destructor simply inlines operator delete, achieving true zero-cost abstraction.
02Custom Deleters for Legacy C APIs
While std::unique_ptr defaults to calling delete, it can be parameterized with a custom deleter to manage any operating system resource: C FILE* handles, POSIX file descriptors, OpenSSL contexts, or custom arena allocations.
When using a stateless lambda or struct deleter, std::unique_ptr exploits the Empty Base Optimization (EBO), preserving its 8-byte zero-overhead size.
However, if a function pointer is passed as a deleter (e.g., std::unique_ptr<FILE, int(*)(FILE*)>), the pointer size doubles to 16 bytes to store the function pointer address.
03Why std::make_unique Is Essential for Exception Safety
Prior to C++14, creating a unique_ptr required writing std::unique_ptr<T>(new T()). This syntax introduced a subtle exception-safety vulnerability in function calls with multiple arguments.
Consider process(std::unique_ptr<A>(new A()), std::unique_ptr<B>(new B())). C++ does not mandate the order of argument evaluation. A compiler could execute: 1. new A(), 2. new B(), 3. construct unique_ptr<A>, 4. construct unique_ptr<B>. If step 2 throws an exception, the memory allocated in step 1 is permanently leaked!
std::make_unique guarantees atomic allocation and pointer construction, eliminating this vulnerability completely while providing cleaner syntax.
04Ownership Comparison Matrix
Understanding the ownership semantics of pointer types in modern C++:
Follow the Core Guidelines: Use std::unique_ptr by default; use raw pointers only for non-owning parameter passing (or std::span/string_view).
+-----------------------+---------------------+-------------------+-------------------------+| Pointer Type | Ownership Semantic | Copyable? | Memory Overhead |+-----------------------+---------------------+-------------------+-------------------------+| T* (Raw Pointer) | Non-owning observer | YES | 8 bytes (0 overhead) || std::unique_ptr<T> | Exclusive single | NO (Move-only) | 8 bytes (0 overhead) || std::shared_ptr<T> | Shared reference | YES | 16 bytes + Control Block|| std::weak_ptr<T> | Non-owning observer | YES | 16 bytes (Ref to CB) |+-----------------------+---------------------+-------------------+-------------------------+05Production Systems Architecture & Hardware Trade-Offs
In industrial-grade systems development, resource management extends beyond heap memory deallocation. Critical systems must reliably manage operating system file descriptors, epoll event loops, GPU CUDA streams, and hardware crypto acceleration handles. Relying on manual cleanup routines inevitably introduces resource leaks along unexpected error propagation paths, ultimately causing file descriptor exhaustion and server unresponsiveness.
By adhering strictly to the RAII idiom, engineers ensure that resource lifecycles are bound deterministically to object scope. Whether a function exits normally, returns early due to a validation failure, or unwinds the stack following an unhandled exception, destructors execute in exact reverse order of construction, guaranteeing leak-free reclamation without garbage collection pauses.
The ideas worth keeping.
- std::unique_ptr enforces exclusive single ownership with zero runtime memory overhead.
- Custom deleters allow std::unique_ptr to manage any C API handle or OS resource.
- Always use std::make_unique to guarantee exception safety and clean code.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Write a factory function named create socket
Write a factory function named create_socket(int port) that returns a std::unique_ptr<Socket> configured with port, using std::make_unique.
Explore a worked solution
struct Socket { int port; Socket(int p) : port(p) {} };#include <memory>[[nodiscard]] std::unique_ptr<Socket> create_socket(int port) { return std::make_unique<Socket>(port);}Use std::make_unique<Socket>(port).
Exercise 2: Explain why attempting to pass a std unique
Explain why attempting to pass a std::unique_ptr by value into a function without std::move causes a compilation failure.
Explore a worked solution
// Passing by value invokes the copy constructor.// std::unique_ptr explicitly deletes its copy constructor to enforce single exclusive ownership.// Attempting to copy triggers: 'call to deleted constructor of std::unique_ptr'.// Fix: Pass by moving: consume(std::move(my_widget)); or pass by non-owning reference/pointer.std::unique_ptr cannot be copied; it is move-only.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Write a factory function named create socket
Write a factory function named create_socket(int port) that returns a std::unique_ptr<Socket> configured with port, using std::make_unique.
Build completed. Binary executed. [Exit code: 0]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.