LESSON 13 · INTERMEDIATE4 min read

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.

C++20 / C++23Free to readModule 4 of 11
YOUR NEXT SMALL WINS

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.

example.js⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

example.js⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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).

notes.txtPlain text
+-----------------------+---------------------+-------------------+-------------------------+| 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.

LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
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).

02

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.

your_turn_2.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
// 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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01How much additional memory does a std::unique_ptr<int> with a default deleter occupy compared to a raw int* pointer?
02Why should developers use std::make_unique rather than std::unique_ptr<T>(new T())?
03How can ownership of a managed object be transferred from one std::unique_ptr to another?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
Build completed. Binary executed.
[Exit code: 0]
main.cppC++
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

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.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.