Zero-cost unique_ptr deleters
Smart pointers are a major feature of C++11 that changed the way the language works.
They come in two versions:
shared_ptr for shared ownership and
unique_ptr for exclusive ownership.
unique_ptr represents an owning pointer. That's just like a good old
T * pointer, except the compiler knows it owns its target, and will call
delete for us
And it does this with no overhead: a
unique_ptr uses exactly the same amount of memory as a
regular pointer, and has zero runtime cost. And that is what we love about C++.
Why a custom deleter?
I happen to have this amazing C library that I want to use:
Nothing complex so far. Assuming the header file is properly written, I can use it from C++ as usual:
Great! Just be sure to close the api, we don't want to leak it. It's fine, we are all great developers who would never forget the closing part. And will never recruit any intern or forgetful colleague. Wait… what happens if any operation in the do things part throws an exception?
Now would be a good time to review Herb Sutter's Guru of the Week #20 on code complexity. It shows how a very simple function has 3 regular code paths and 20 invisible exceptional paths, many of which even experienced developers don't identify correctly.
Well… on exception we leak
api. Not good.
In fact, we lost all the
safety goodness that
unique_ptr usually provides us.
api is a pointer to some allocated memory right? Why don't we use
The issue of course, is
unique_ptr is not aware of
close_my_api() and attempts to clear
delete. It may or may not crash on your system, depending on whether
free under the hood. In any case, it will never invoke
But the template library has a solution: we can provide a custom deleter.
Working naive approach
Indeed, the full signature of
unique_ptr reads like this:
So provided we fill in the correct template arguments, we can tell
unique_ptr to use
close_my_api() instead of
delete to clean up:
There is nothing wrong with this approach. It is correct and somewhat intuitive. In fact, this approach is the one you usually find on StackOverflow.
But it is not zero-cost.1
The size of the
api pointer increased from 8 bytes to 16 bytes. That's a 100% increase.
The thing is we are now storing a copy of the address of
unique_ptr. Which is a complete waste, since we know for sure all
objects will always be deleted with
This is C++. We want zero-cost. Surely there must be a better way?
Coming back to the template signature of
unique_ptr, we notice it uses and odd
default_delete default deleter. How about customizing its behavior, like
this answer suggests?
It would seem to work, except this is illegal. Although specializing
is indeed allowed, it must follow the rules:
A program may add a template specialization […] only if […] the specialization meets the standard library requirements for the original template.
So what are the exact rules on
default_delete::operator()? Section 184.108.40.206.2, item 3 reads:
Effects: calls delete on ptr.
In other words: specializing
default_delete is allowed only if the effects of the specialization are to call
delete on the pointer.
➥ Thus our specialization does not meet the requirements for
and is not allowed by the C++ standard.
But we are on the right track.
Instead of hijacking
std::default_delete, let's create our own.
As the existence of
default_delete hints, the Deleter argument of
allows function objects. That is, objects that behave as functions (supporting
This is exaclty what we will do. We use an empty object with just
close_my_api() correctly. Yet it does not store the pointer to it,
as we can see from the final pointer size being 8 bytes.
What actually happens is that an instance of
external_api_deleter is default-constructed
and stored. As it is an empty class, this has perfect performance (this generates no
assembly instruction). And thanks to
empty base optimisation, is uses no storage
There we have it. Legal, correct, zero-cost deleter for
I hope you enjoyed this post. I pushed the code of the examples on github. Have a question? See an error? Send me a message!
In addition, such a↩
unique_ptrcannot be default-constructed, because the deleter pointer must always be provided. This can be a minor inconvenience, or have a big impact. For instance it prevents using map's
This is not guaranteed, but true in practice on all systems I tested this one.↩