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 malloc using std::free.
  • Closing POSIX file descriptors or C FILE* handles using fclose.
  • 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_ptr incurs 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_ptr instance. This increases the size of the smart pointer (e.g., using a raw function pointer typically doubles the size to 2 * 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 types

Custom 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:

  1. The deleter is copied or moved into the dynamically allocated control block created by the smart pointer.
  2. The control block contains virtual functions or function pointers that know how to invoke and destroy the stored deleter.
  3. The std::shared_ptr object 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