C++ Custom Deleters | 'Custom Deleter' Guide

C++ Custom Deleters | 'Custom Deleter' Guide

이 글의 핵심

When custom deleters are needed, comparing function pointers·lambdas·function objects, differences in type·storage between unique_ptr and shared_ptr, file·socket RAII, and performance.

What is Custom Deleter?

Customizing deletion behavior of smart pointers

Below is an implementation example using C++. Try running the code directly to check its operation.

auto deleter = [](int* p) {
    std::cout << "Deleting: " << *p << std::endl;
    delete p;
};

std::unique_ptr<int, decltype(deleter)> ptr(new int(10), deleter);

When Custom Deleters Are Needed

When cleanup other than standard delete/delete[] is needed, use custom deleters.

  • C API/Platform Handles: FILE* (fclose), socket descriptors (close / closesocket), HANDLE, DIR*, etc.
  • Allocation Method Mismatch: malloc/free, returning to custom pool, custom aligned allocation, etc.
  • Arrays: When holding new T[n] with shared_ptr<T> and need delete[] (legacy code), call delete[] in deleter. Prefer std::unique_ptr<T[]> / std::vector / C++17 shared_ptr<T[]> when possible.
  • Observation·Debugging: When adding logging or profiling hooks at deletion.
  • Third-party Objects: When there’s a contract like “this pointer must only be released with library X’s release”.

Smart pointers are tools that bundle ownership + cleanup at scope end, and custom deleters are the part that changes “what to call at scope end”. This directly relates to the “Relationship with RAII” section below.

Function Pointer vs Lambda vs Function Object

MethodCharacteristicsIn unique_ptrIn shared_ptr
Function PointerNo state, type fixed as void(*)(T*)Deleter size is one pointerStored in control block, type still shared_ptr<T>
Stateless Lambdadecltype(lambda) is unique type (different for each lambda)Different Del makes different unique_ptr typesDeleter stored in control block at runtime, can be treated as same shared_ptr<T>
Capturing LambdaDeleter object grows by capture sizeunique_ptr object size increases (empty base optimization may not apply)Copied·moved to control block
Function Object (struct)Easy to specify template·state·noexcept in operator()Good for team convention reuseSame

Production Guide: For named deleters reused by team, use function object like struct FileCloser { void operator()(FILE*) const; } for easier testing·documentation·single definition. Stateless lambdas are common for one-liners. Function pointers are used when directly connecting with C API or wanting to fix binary size.

Difference in unique_ptr vs shared_ptr (Deleter Perspective)

  • std::unique_ptr<T, D>: Deleter type D is template argument, so different D means different unique_ptr type itself. Assignment like ptr1 = ptr2 only works with same T and same D.
  • std::shared_ptr: Deleter is type-erased with pointer type T* in control block. So shared_ptr<int> created with different lambdas/deleters are treated as same static type and can be assigned·compared (assuming managed object is valid).

Performance: Stateless deleters in unique_ptr often get compiler optimization without extra storage. shared_ptr deleter calls may be indirect calls, so profiling is recommended for ultra-high-frequency paths.

Relationship with RAII

RAII is “resource acquisition is initialization and cleanup in destructor”. When using C API, destructor is just a function like fclose, so putting custom deleter in smart pointer allows unique_ptr<FILE, FileCloser> form to align with same scope rules for C++ objects. The fact that deleter is called by stack unwinding even on exception is same as regular RAII classes. Use shared_ptr only when ownership sharing is needed; for single ownership, unique_ptr with deleter type exposed at compile-time is more explicit in intent.

Performance Considerations (Deleter Itself)

  • unique_ptr + stateless deleter: Almost no overhead, often just holding T* pointer.
  • unique_ptr + heavy deleter: Deleter object increases unique_ptr object size. Avoid capturing more than necessary.
  • shared_ptr: Reference count updates and control block are base costs. Deleter is usually one indirect call inside control block. Rather than using shared_ptr just for custom deleter, better to first judge if sharing is really needed.

make_shared and allocation count - when creating shared_ptr with custom deleter, often can’t use make_shared, resulting in 2 allocations, which should be considered in cost.

unique_ptr Deleter

Below is an implementation example using C++. Understand the role of each part while examining the code.

// Function pointer
void myDeleter(int* p) {
    std::cout << "Deleting" << std::endl;
    delete p;
}

std::unique_ptr<int, decltype(&myDeleter)> ptr(new int(10), myDeleter);

// Lambda
auto deleter = [](int* p) { delete p; };
std::unique_ptr<int, decltype(deleter)> ptr2(new int(20), deleter);

shared_ptr Deleter

Below is an implementation example using C++. Try running the code directly to check its operation.

// shared_ptr: not included in type
auto deleter = [](int* p) {
    std::cout << "Deleting" << std::endl;
    delete p;
};

std::shared_ptr<int> ptr(new int(10), deleter);

Practical Examples

Example 1: FILE* Management

Here is detailed implementation code using C++. Import the necessary modules and perform branching with conditionals. Understand the role of each part while examining the code.

#include <cstdio>

auto fileDeleter = [](FILE* f) {
    if (f) {
        std::cout << "Closing file" << std::endl;
        std::fclose(f);
    }
};

std::unique_ptr<FILE, decltype(fileDeleter)> file(
    std::fopen("data.txt", "r"),
    fileDeleter
);

if (file) {
    char buffer[100];
    std::fgets(buffer, sizeof(buffer), file.get());
}

Example 2: Array Deletion

Below is an implementation example using C++. Try running the code directly to check its operation.

// ❌ Using delete (arrays need delete[])
// std::unique_ptr<int> ptr(new int[10]);

// ✅ Array specialization
std::unique_ptr<int[]> ptr(new int[10]);

// ✅ Custom deleter
auto deleter = [](int* p) { delete[] p; };
std::unique_ptr<int, decltype(deleter)> ptr2(new int[10], deleter);

Example 3: C Library Resources

Below is an implementation example using C++. Import the necessary modules. Understand the role of each part while examining the code.

#include <cstdlib>

// malloc/free
auto deleter = [](int* p) {
    std::cout << "free" << std::endl;
    std::free(p);
};

std::unique_ptr<int, decltype(deleter)> ptr(
    static_cast<int*>(std::malloc(sizeof(int))),
    deleter
);

Example 4: Socket Management

In POSIX environments, fd is often int. Below is an example of putting one int on heap and passing pointer to deleter (in production, rather than specializing unique_ptr for int itself, often use wrapper with different type as shown in “Alternative”).

Here is detailed implementation code using C++. Import the necessary modules, define a class to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part while examining the code.

#include <unistd.h>  // close

struct SocketDeleter {
    void operator()(int* sock) const {
        if (sock && *sock >= 0) {
            ::close(*sock);
            std::cout << "Closing socket" << std::endl;
        }
        delete sock;
    }
};

extern int createSocket();  // e.g., socket(...)

std::unique_ptr<int, SocketDeleter> socket(new int(createSocket()));

Alternative: When only int fd exists, define FdCloser to call close instead of std::default_delete<int> in unique_ptr<int, FdCloser>, and align with team rule marking “invalid fd” as -1 to close without leaks even in exception paths. Windows uses closesocket and SOCKET type.

Summary

Key Points

  1. Custom deleter: Customize cleanup behavior
  2. Function pointer/lambda/function object: Different characteristics
  3. unique_ptr: Deleter type in template argument
  4. shared_ptr: Deleter type-erased in control block
  5. RAII: Automatic cleanup at scope end

When to Use

Use custom deleter when:

  • Managing C API resources (FILE*, socket, etc.)
  • Using malloc/free
  • Need custom cleanup logic
  • Managing arrays with shared_ptr (legacy)

Don’t use when:

  • Standard delete is sufficient
  • Adds unnecessary complexity
  • Can use RAII wrapper class instead

Best Practices

  • ✅ Use unique_ptr for single ownership
  • ✅ Use shared_ptr only when sharing needed
  • ✅ Prefer stateless deleters for performance
  • ❌ Don’t capture unnecessarily in lambda
  • ❌ Don’t forget null checks in deleter

Master custom deleters for safe resource management! 🚀