Custom Deleters in C++: unique_ptr vs shared_ptr
In modern C++, smart pointers manage the lifetime of dynamically
allocated resources automatically, but non-memory resources or
non-standard allocations require specialized cleanup. A custom deleter
is a callable object—such as a function, functor, or lambda—invoked
instead of the standard delete operator when a managed
resource is destroyed. This article explains the mechanics of custom
deleters and details the fundamental architectural difference in how
std::unique_ptr and std::shared_ptr store and
manage deleter types.
What Is a Custom Deleter?
By default, std::unique_ptr and
std::shared_ptr invoke the standard delete (or
delete[]) operator when releasing managed objects. A custom
deleter replaces this behavior, allowing smart pointers to manage any
resource that requires explicit cleanup. Typical use cases include:
- Freeing memory allocated via
mallocusingstd::free. - Closing POSIX file descriptors or C
FILE*handles usingfclose. - Releasing OS-level handles (e.g., Win32
HANDLE, sockets, database connections).
Custom Deleters in std::unique_ptr
In std::unique_ptr, the deleter's type is hardcoded as
part of the class template signature:
template <class T, class Deleter = std::default_delete<T>>
class unique_ptr;Because the deleter is a template argument, the exact type must be known at compile time.
Storage and Memory Footprint
- Stateless Deleters (Functors and Capturing-Free
Lambdas): Leveraging the Empty Base Optimization (EBO), or
[[no_unique_address]]in C++20,std::unique_ptrincurs zero space overhead. The pointer remains the exact size of a single raw pointer (sizeof(void*)). - Function Pointers and Stateful Deleters: If a plain
function pointer or a lambda with captures is used, the deleter must be
stored alongside the raw pointer inside the
std::unique_ptrinstance. This increases the size of the smart pointer (e.g., using a raw function pointer typically doubles the size to2 * sizeof(void*)).
Type System Implications
Because the deleter is embedded in the type, pointers with different deleters are incompatible types:
auto file_closer = [](FILE* fp) { if (fp) fclose(fp); };
std::unique_ptr<FILE, decltype(file_closer)> p1(fopen("test.txt", "r"), file_closer);
std::unique_ptr<FILE, void(*)(FILE*)> p2(fopen("test.txt", "r"), &fclose);
// p1 and p2 have different, non-interchangeable typesCustom Deleters in std::shared_ptr
Unlike std::unique_ptr, std::shared_ptr
does not include the deleter type in its class template signature:
template <class T>
class shared_ptr;Instead, the deleter is passed as an argument to the constructor:
std::shared_ptr<FILE> file(fopen("test.txt", "r"), &fclose);Storage via Type Erasure
std::shared_ptr achieves this flexibility through
type erasure. When a std::shared_ptr is
constructed with a custom deleter:
- The deleter is copied or moved into the dynamically allocated control block created by the smart pointer.
- The control block contains virtual functions or function pointers that know how to invoke and destroy the stored deleter.
- The
std::shared_ptrobject itself remains constant in size (typically two raw pointers: one pointing to the managed object, and one pointing to the control block), regardless of the deleter's type or size.
Type System Implications
Because the deleter is not part of the std::shared_ptr
type:
- Any
std::shared_ptr<T>can be stored in the same container or passed to the same function regardless of how each instance deletes its resource. - Pointers using different cleanup functions can be freely assigned and compared to one another.
Note: Custom deleters cannot be used with
std::make_shared, because std::make_shared
allocates both the control block and the managed object in a single
contiguous block using standard allocation.
Key Differences Summary
| Feature | std::unique_ptr |
std::shared_ptr |
|---|---|---|
| Type Definition | Part of the template type:
unique_ptr<T, Deleter> |
Not part of the template type:
shared_ptr<T> |
| Binding Time | Compile time | Runtime |
| Storage Location | Directly inside the
unique_ptr object |
In the dynamically allocated control block |
| Memory Overhead | Zero overhead if empty/stateless; grows if stateful | Fixed pointer size; deleter stored in control block |
| Interchangeability | Different deleters yield distinct, incompatible types | Different deleters share the same type |