Custom Deleter for C++ Smart Pointers
Introduction
We sometimes see custom deleters used for C++ smart pointers. Previously, I have not thought too much about why and when we have to use custom deleters, and what the efficient way to include custom deleters is.
In this blog post, I would like to discuss the efficient usages and motivations of custom deleters for C++ std::unique_ptr
and std::shared_ptr
.
Custom Deleter
Example
1 |
|
Execution and Memory Leak Check
1 | $ g++ smart_ptr_custom_deleter.cpp -o smart_ptr_custom_deleter |
Discussions
When do we need custom deleters for smart pointers?
Usually it is when we need to use the objects from some C libraries for our application. Objects from C library do not have destructor and usually requires specific resource free functions to free the memory allocated on heap. If all the objects are from C++ libraries and have well-defined destructor, we can just create std::unique_ptr
and std::shared_ptr
without deleters.
Why don’t std::make_unique and std::make_shared accept custom deleters?
std::make_unique
and std::make_shared
do not accept custom deleters. Their purposes are mostly for replacing the new
/delete
behaviors for C++ objects. If we ever have to use custom deleters, we use std::unique_ptr
and std::shared_ptr
instead.
Why std::unique_ptr carries deleter type as its part of type whereas std::shared_ptr does not?
Most likely it is because of the performance. std::shared_ptr
always carries control block to track the object sharing status and is thus less efficient. Creating a copy of an additional custom deleter will not likely affect its size and performance significantly. std::unique_ptr
, however, is designed for more efficient purposes and tries to be as efficient as the raw pointer. With a stateless deleter and possibly compile-time empty base optimization, the deleter code could be inlined and there is no additional memory required to store the deleter, therefore maintaining the performance of std::unique_ptr
. For example, std::FILE*
, std::unique_ptr<std::FILE>
, and std::unique_ptr<std::FILE, FileCloser>
are all 8 Bytes. So using stateless functor deleter for std::unique_ptr
is as efficient as the raw pointer. However, if the optimization fails to be applied, the size of std::unique_ptr
could go arbitrarily large as the deleter could have state and go arbitrarily large. std::shared_ptr
data structure only has the raw pointer and a pointer to the control block, so its size is always fixed and usually twice as large as std::unique_ptr
.
What is the control block for std::shared_ptr?
The control block is a dynamically-allocated object that holds:
- either a pointer to the managed object or the managed object itself;
- the deleter (type-erased);
- the allocator (type-erased);
- the number of
std::shared_ptr
s that own the managed object; - the number of
std::weak_ptr
s that refer to the managed object.
The reference counts update uses atomic instructions which brings some overhead when std::shared_ptr
gets copied. However, the dereferencing cost are exactly the same as the raw pointer.
References
Custom Deleter for C++ Smart Pointers
https://leimao.github.io/blog/CPP-Smart-Pointer-Custom-Deleter/