Custom Deleters — Function Objects, Lambda Deleters, Resource Handles
In this tutorial, you will learn about Custom Deleters. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ custom deleters extend smart pointer RAII beyond heap memory to arbitrary resources like file handles, sockets, and database connections using function objects, lambdas, and dedicated deleter types.
What You'll Learn
You will write custom deleters for unique_ptr and shared_ptr, use lambda expressions as inline deleters, create reusable deleter function objects for common resource types, handle resources other than heap memory (files, sockets, mutexes), and understand how custom deleters affect smart pointer type and size.
Why It Matters
RAII is not just for memory. Every resource — file handles, network sockets, database connections, mutex locks, GPU buffers — benefits from deterministic cleanup. Custom deleters let you apply smart pointer semantics to any resource that has acquire/release semantics, eliminating resource leaks even in the presence of exceptions.
Learning Path
graph LR
A["24: Smart Pointers"] --> B["25: Custom Deleters"]
B --> C["26: Allocators"]
C --> D["27: Object Lifetimes"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Custom Deleter with unique_ptr
#include <iostream>
#include <memory>
#include <cstdio>
// Function as deleter
void fileDeleter(std::FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed via function\n";
}
}
int main() {
// unique_ptr with function pointer deleter
std::unique_ptr<std::FILE, decltype(&fileDeleter)> fp1(
std::fopen("test.txt", "w"), fileDeleter);
// unique_ptr with lambda deleter
auto lambdaDeleter = [](std::FILE* f) {
if (f) {
std::fclose(f);
std::cout << "File closed via lambda\n";
}
};
std::unique_ptr<std::FILE, decltype(lambdaDeleter)> fp2(
std::fopen("test2.txt", "w"), lambdaDeleter);
}
Function Object Deleters
#include <iostream>
#include <memory>
template <typename T>
struct DeleterFor {
void operator()(T* ptr) const {
std::cout << "Generic delete for type\n";
delete ptr;
}
};
template <>
struct DeleterFor<std::FILE> {
void operator()(std::FILE* ptr) const {
if (ptr) {
std::fclose(ptr);
std::cout << "FILE closed via specialized deleter\n";
}
}
};
int main() {
std::unique_ptr<int, DeleterFor<int>> intPtr(new int(42));
std::unique_ptr<std::FILE, DeleterFor<std::FILE>> filePtr(
std::fopen("test.txt", "w"));
}
Socket Resource Handle
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/socket.h>
struct SocketDeleter {
void operator()(int* fd) const {
if (fd && *fd >= 0) {
close(*fd);
std::cout << "Socket " << *fd << " closed\n";
}
delete fd;
}
};
using SocketPtr = std::unique_ptr<int, SocketDeleter>;
SocketPtr createSocket() {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return SocketPtr(nullptr, SocketDeleter{});
return SocketPtr(new int(fd), SocketDeleter{});
}
int main() {
SocketPtr sock = createSocket();
if (sock) {
std::cout << "Socket " << *sock << " created\n";
}
// Socket is automatically closed when sock goes out of scope
}
Custom Deleter with shared_ptr
#include <iostream>
#include <memory>
struct DatabaseConnection {
void connect() { std::cout << "Connected to DB\n"; }
void disconnect() { std::cout << "Disconnected from DB\n"; }
void query(const char* sql) {
std::cout << "Query: " << sql << "\n";
}
};
struct DBDeleter {
void operator()(DatabaseConnection* db) const {
if (db) {
db->disconnect();
delete db;
}
}
};
int main() {
auto dbDeleter = [](DatabaseConnection* db) {
if (db) {
db->disconnect();
delete db;
}
};
std::shared_ptr<DatabaseConnection> db(
new DatabaseConnection(),
dbDeleter
);
db->connect();
db->query("SELECT * FROM users");
// disconnect() called automatically when db is destroyed
}
How Custom Deleters Affect Type and Size
#include <iostream>
#include <memory>
int main() {
// Default deleter: pointer-size only
std::unique_ptr<int> defaultPtr;
std::cout << "Default deleter size: " << sizeof(defaultPtr) << "\n";
// Function pointer deleter: pointer + function pointer
auto funcDel = [](int* p) { delete p; };
std::unique_ptr<int, decltype(funcDel)> funcPtr;
std::cout << "Lambda (stateless) size: " << sizeof(funcPtr) << "\n";
// Stateless lambda: same as pointer (empty class optimization)
// Stateful lambda: adds state size
int x = 0;
auto statefulDel = [x](int* p) mutable { delete p; };
std::unique_ptr<int, decltype(statefulDel)> statePtr;
std::cout << "Lambda (stateful) size: " << sizeof(statePtr) << "\n";
}
Stateless lambdas and empty function objects benefit from the empty base optimization, so unique_ptr with them is the same size as a raw pointer.
Type Erasing Custom Deleters
#include <iostream>
#include <memory>
#include <functional>
// shared_ptr type-erases the deleter (constructor parameter, not template)
void demonstrateTypeErasure() {
// unique_ptr: deleter is part of the type
// std::unique_ptr<FILE, ???> — type must be known
// shared_ptr: deleter type is erased
std::shared_ptr<std::FILE> file(
std::fopen("test.txt", "w"),
[](std::FILE* f) { if (f) std::fclose(f); }
);
// The lambda's type is not part of shared_ptr<FILE>
// This enables heterogeneous deleters in containers
}
// Using std::function for runtime deleters
struct RuntimeDeleter {
std::function<void(void*)> deleter;
template <typename T>
RuntimeDeleter(T&& d) : deleter(std::forward<T>(d)) {}
void operator()(void* ptr) const {
if (deleter) deleter(ptr);
}
};
int main() {
// Use std::function as deleter for maximum flexibility
auto customDel = [](int* p) {
std::cout << "Custom deletion\n";
delete p;
};
std::unique_ptr<int, std::function<void(int*)>> up(
new int(42), customDel);
}
Common Mistakes
Mistake 1: Forgetting to Handle Null in Custom Deleter
void myDeleter(FILE* f) { fclose(f); } // crashes if f is null
Always check for null before releasing.
Mistake 2: Deleter that Does Not Deallocate
If your custom deleter does not call delete for heap-allocated objects, you have a memory leak. For non-memory resources, this is intentional.
Mistake 3: Using unique_ptr with Stateful Lambda in a Container
unique_ptr type includes the lambda type. Stateful lambdas have unique types, making heterogeneous containers impossible.
Mistake 4: Calling delete on Resources Not Allocated with new
std::unique_ptr<int, SomeDeleter> ptr(new int); // fine
std::unique_ptr<int, SomeDeleter> ptr(malloc(sizeof(int))); // WRONG
If you allocate with malloc, the deleter must call free, not delete.
Mistake 5: Exception-Safety in Complex Deleters
Custom deleters should be noexcept. If a deleter throws during stack unwinding (when another exception is active), std::terminate is called.
Mistake 6: Type Mismatch Between Pointer and Deleter
std::unique_ptr<Base, BaseDeleter> ptr(new Derived());
// If BaseDeleter calls delete on Base*, but object is Derived*
// This requires Base destructor to be virtual
Practice Questions
- Why might you need a custom deleter for
unique_ptr? - How does a custom deleter change the size of a
unique_ptr? Ashared_ptr? - Write a custom deleter that logs the destruction of an object.
- Why does
shared_ptrtype-erase the deleter butunique_ptrdoes not? - Implement a
MutexLockRAII wrapper using a custom deleter forpthread_mutex_t.
Challenge
Design an AnyResource class that wraps any resource with a type-erased cleanup function. Use std::shared_ptr<void> with a custom deleter that calls the appropriate cleanup. Demonstrate with FILE*, int (socket fd), and a custom DatabaseHandle.
FAQ
Mini Project
Build an RAII wrapper for POSIX shared memory:
#include <iostream>
#include <memory>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
struct SharedMemoryDeleter {
size_t size_;
void operator()(void* ptr) const {
if (ptr != MAP_FAILED) {
munmap(ptr, size_);
std::cout << "Shared memory unmapped\n";
}
}
};
std::unique_ptr<void, SharedMemoryDeleter>
createSharedMemory(const char* name, size_t size) {
int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
if (fd < 0) return {nullptr, SharedMemoryDeleter{size}};
ftruncate(fd, size);
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
return {ptr, SharedMemoryDeleter{size}};
}
int main() {
size_t size = 4096;
auto mem = createSharedMemory("/myshm", size);
if (mem) {
int* data = static_cast<int*>(mem.get());
data[0] = 42;
data[1] = 99;
std::cout << "Wrote " << data[0] << " and " << data[1] << "\n";
}
// Memory unmapped automatically
shm_unlink("/myshm");
}
What's Next
Custom deleters generalize RAII beyond memory. The next lesson covers allocators: std::allocator, custom allocators, and pool allocation strategies for controlling how containers acquire memory.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro