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]withshared_ptr<T>and needdelete[](legacy code), calldelete[]in deleter. Preferstd::unique_ptr<T[]>/std::vector/ C++17shared_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
| Method | Characteristics | In unique_ptr | In shared_ptr |
|---|---|---|---|
| Function Pointer | No state, type fixed as void(*)(T*) | Deleter size is one pointer | Stored in control block, type still shared_ptr<T> |
| Stateless Lambda | decltype(lambda) is unique type (different for each lambda) | Different Del makes different unique_ptr types | Deleter stored in control block at runtime, can be treated as same shared_ptr<T> |
| Capturing Lambda | Deleter object grows by capture size | unique_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 reuse | Same |
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
Dis template argument, so differentDmeans differentunique_ptrtype itself. Assignment likeptr1 = ptr2only works with sameTand sameD. - std::shared_ptr
: Deleter is type-erased with pointer type T*in control block. Soshared_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 holdingT*pointer.unique_ptr+ heavy deleter: Deleter object increasesunique_ptrobject 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_ptrjust 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
- Custom deleter: Customize cleanup behavior
- Function pointer/lambda/function object: Different characteristics
- unique_ptr: Deleter type in template argument
- shared_ptr: Deleter type-erased in control block
- 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
Related Articles
- C++ make_unique/make_shared
- C++ RAII and Smart Pointers
- C++ nullptr vs NULL
Master custom deleters for safe resource management! 🚀