Understanding the inner workings of C++ smart pointers - The unique_ptr with custom deleter
Let's continue where I left off last time. You've seen a simple implementation of a unique_ptr
in Understanding the inner workings of C++ smart pointers - The unique_ptr. Now, let's improve that model by adding a custom deleter as the Standard Library does.
Applications of a custom deleter
Let's first establish why somebody would want a custom deleter.
One example is that the object was allocated via a local heap, and such must be returned by calling the corresponding deallocation function.
Another example is fopen
. This function returns a FILE*
object that you are supposed to delete by calling fclose
. A classic job for a unique pointer. But you cannot call delete
on the FILE
pointer.
Here are two examples of using a unique_ptr
with a custom deleter.
1 2 3 4 5 6 7 8 9 10 |
|
Oh yes, the first object, alfred
, doesn't provide a custom deleter. Only robin
does. Behind the curtains, both do. Let's look at a modified unique_ptr
implementation that handles the custom deleter case.
unique_ptr
implementation with custom deleter support
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
The major difference to the previous implementation I presented you in Understanding the inner workings of C++ smart pointers - The unique_ptr is that this unique_ptr
takes a second defaulted template parameter A.
The default is DefaultDeleter
which in its call operator calls delete
. If you do not provide a second argument to a unique_ptr
, then the STL creates a default deleter for you, typed with the unique_ptr
s pointer type.
As a result of the default deleter, the unique_ptr
no longer stores the plain pointer. Instead, in B, you can see a new type, compressed_pair
.
In C, you can see the constructor from the previous version, which now invokes the constructor of compressed_pair
. But there is a second constructor D, which takes a data pointer and a pointer to a deleter. Subsequently, this constructor also invokes the constructor of compressed_pair
.
Influenced by this change, the destructor isn't invoked directly any longer. Instead, the destructor calls second()
on the compressed pair, passing first()
as a parameter. Remember the DefaultDeleter
? The implementation assumes that a deleter is a function taking the data pointer as a parameter.
I omitted the move operations deliberately.
unique_ptr
s trick: compressed_pair
How does the compressed_pair
implementation look like? Here is one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
compressed_pair
is a class template with a specialization for the case when the custom deleter is a class without any data member, an empty class. The primary template F isn't that interesting. You have a class with two data members. The data pointer and a pointer to the deleter. While first()
returns the data pointer, second()
returns the deleter.
More interesting is the specialization G. There is a general optimization in C++ called the empty base class optimization (EBO). That optimization can be applied if a class derives from an empty base class. In that case, the compiler does not need to reserve space for the base class. This is exactly what compressed_pair
does. In H, you can see that the specialization derives from the deleter. Consequently, the specialization of compressed_pair
only has one data member, the data pointer. If you invoked second()
, you get a pointer to this
.
With this clever trick, the unique_ptr
aims to require the least amount of storage space. The default deleter above uses exactly this trick. At the same time, you pay more in the case of MyDeleter
because the compiler stores a pointer to the function.
More to come
Next time, I will show you the internals of a shared_ptr
, including the control block and make_shared
.
Andreas