Custom Deleters in Unique and Shared Pointers
Using Resource Acquisition is Initialization (RAII), we can tie the lifetime of an resource (locks, file handles, database connection etc.) to the lifetime of the holding object. And, when the object is deleted, the underlying resource is freed. This helps us write leak-free code.
Local objects, not allocated on the heap are destructed when they go out of scope. You can release the resource in the destructor, that you had acquired on construction. For most use-cases, having non-heap local objects would suffice and this is the safest way to not leak resources. For objects that are allocated on the heap, we can use an std::unique_ptr<T>
or an std::shared_ptr<T>
to get the objects scoped to a class or a block. And when the objects goes out of scope, the destructors are called on the objects, even in the case of exceptions, where again you can release the resources.
int main(int argc, char *argv[])
{
// Locally scoped
SomeRes local_obj;
// Also locally scoped
std::unique_ptr<SomeRes> u_res = std::make_unique<SomeRes>();
std::shared_ptr<SomeRes> s_res = std::make_shared<SomeRes>();
// Do work
return 0;
}
// s1 is destructed
// u1 is destructed
// local_obj is destructed
// The objects are destructed even in the case of exceptions/early returns
Now, let us say, we want to work with a legacy C pointer like FILE *
(for some reason) to do I/O. The wrong way to use such a resource would be:
void use_res()
{
FILE *fp = fopen("some_file", "r");
// Do work
fclose(fp);
}
In this case, if the do work part returns early or if an exception is thrown, we have now leaked the file pointer since we did not get a chance to call fclose()
on it. The right way would be to use a std::unique_ptr<FILE>
or an std::shared_ptr<FILE>
depending on your ownership requirements. The code would now be (which is again wrong):
void use_res()
{
// Warning: Wrong!!!
std::unique_ptr<FILE> fp(fopen("some_file", "r"));
// Do work
}
// Expect fp to be released here
The compiler does not complain and when you then run this code, you leak the resource. This can be traced using tools such as Address Sanitizer or valgrind. I have enabled address sanitizer on my build and I get the following printed out to my console:
>../bin/debug/workings
=================================================================
==6461==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x7fff8d4fc1a0 in thread T0
#0 0x10b5d9b42 in wrap__ZdlPv (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x6db42)
#1 0x10b5521dd in std::__1::default_delete<__sFILE>::operator()(__sFILE*) const memory:2339
#2 0x10b55215e in std::__1::unique_ptr<__sFILE, std::__1::default_delete<__sFILE> >::reset(__sFILE*) memory:2652
#3 0x10b552098 in std::__1::unique_ptr<__sFILE, std::__1::default_delete<__sFILE> >::~unique_ptr() memory:2606
#4 0x10b54b524 in std::__1::unique_ptr<__sFILE, std::__1::default_delete<__sFILE> >::~unique_ptr() memory:2606
#5 0x10b54b463 in use_res() main.cpp:80
#6 0x10b54b557 in main main.cpp:83
#7 0x7fff636e67fc in start (libdyld.dylib:x86_64+0x1a7fc)
Address 0x7fff8d4fc1a0 is a wild pointer.
SUMMARY: AddressSanitizer: bad-free (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x6db42) in wrap__ZdlPv
==6461==ABORTING
[1] 6461 abort ../bin/debug/workings
The issue here is that when the std::unique_ptr<FILE>
goes out of scope, the runtime attempts to free FILE *
using free()
. And since the FILE *
is not newed or malloced but instead allocated with fopen()
and should be deallocated with fclose()
. Hence, we need to get the pointer to call fclose()
to deallocate resources when we go out of scope and not call free()
.
This is where the custom deleters come into the picture. While construction, we can specify the custom deleter to be called when the object goes out of scope. And this custom deleter will be called by the runtime when the objects goes out of scope.
We can provide a custom deleter using different ways and we shall see each of them below with examples.
Custom Deleter as a Functor
We can override the operator()
method of a class/struct to provide a custom deleter that gets called when the std::unique_ptr<FILE>
or std::shared_ptr<FILE>
goes out of scope. The code would be something like the below:
struct FileCloser {
void operator()(FILE *fp)
{
if (fp != nullptr) {
fclose(fp);
}
}
};
void use_res() {
std::unique_ptr<FILE, FileCloser> fp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"));
std::shared_ptr<FILE> sfp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"),
FileCloser());
// Do something
}
When the object goes out of scope, the provide custom deleter is called and we can be sure that we have not leaked any resource.
Custom Deleter as a Lambda
We can provide a lambda as a custom deleter to std::unique_ptr<FILE>
and std::shared_ptr<FILE>
. This will be called when the object goes out of scope. The code would be something like the below:
void use_res() {
std::unique_ptr<FILE, std::function<void(FILE *fp)>> fp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"),
[](FILE *fp) {
if (fp != nullptr) {
fclose(fp);
}
});
std::shared_ptr<FILE> sfp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"),
[](FILE *fp) {
if (fp != nullptr) {
fclose(fp);
}
});
// Do something
}
We define the custom deleter to be a function that takes a FILE *
and return void and provide the lambda expression during the object’s construction. When the object goes out of scope, the runtime calls the lambda provided to deallocate the resource.
Custom Deleter as a Function Pointer
We can also provide a pointer to a function as a custom deleter. This function pointed to by the pointer will be called when the object goes out of scope.
void closer_func(FILE *fp) {
if (fp != nullptr) {
fclose(fp);
}
}
void use_res() {
std::unique_ptr<FILE, void (*)(FILE *fp)> fp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"),
closer_func);
std::shared_ptr<FILE> sfp(
fopen("/Users/rajkumar.p/Tmp/dump.in", "r"),
closer_func
);
// Do something
}
The runtime calls into the function pointer when the object goes out of scope and fclose()
is called thereby deallocating the resources.
Using the std::unique_ptr<T>
and std::shared_ptr<T>
, it is now easy to to plug in custom allocate/deallocate methods to RAII pointers. These kind of functionality are also provided by other languages — Context managers in Python, defer in Go, finally {} in Java etc. They give a handle to correctly manage the lifetime of a resource and not leak.
That’s it. For any discussion, tweet here.