Skip to content

Custom Deleters — Function Objects, Lambda Deleters, Resource Handles

DodaTech Updated 2026-06-28 7 min read

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

  1. Why might you need a custom deleter for unique_ptr?
  2. How does a custom deleter change the size of a unique_ptr? A shared_ptr?
  3. Write a custom deleter that logs the destruction of an object.
  4. Why does shared_ptr type-erase the deleter but unique_ptr does not?
  5. Implement a MutexLock RAII wrapper using a custom deleter for pthread_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

Can I change the deleter of a unique_ptr after construction?

No. The deleter is part of the type and is determined at compile time. For unique_ptr, the deleter is typically stored in the object and cannot be changed.

What happens if the custom deleter throws?

If a unique_ptr or shared_ptr deleter throws, the program behavior is undefined. In practice, it may abort. Always make deleters noexcept.

How do custom deleters affect performance?

For unique_ptr with a stateless deleter, there is zero overhead. For shared_ptr, the deleter is stored in the control block and incurs a virtual call overhead.

Can I use a custom deleter to call a C++ object's destructor early?

Yes. The deleter is called when the smart pointer is destroyed or reset. Calling .reset() on a unique_ptr invokes the deleter immediately.

Is there a standard deleter for common non-memory resources?

No, the standard library does not provide deleters for FILE*, socket handles, etc. You need to write them yourself.

How do I handle array cleanup with custom deleters?

Use unique_ptr<T[]> for arrays, or provide a deleter that calls delete[]. The default unique_ptr<T[]> already uses delete[].

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