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.

In fact, 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 as needed.

auto function(const std::string & x)
    auto ptr = std::make_unique<Foo>(x);
    if (!someTest()) { throw Error("someTest failed"); }
    return ptr;
Throw, return, pass it around, it will not leak.

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:

struct external_api {
    int data;
    // other api-relevant state

external_api * open_my_api()
    external_api * api = malloc(sizeof(external_api));
    // initialize state here
    printf("External C api %p opened\n", api);
    return api;

void close_my_api(external_api * api)
    printf("External C api %p closed\n", api);

// Plenty other functions using data in external_api
A feature-packed C library. Actual implementation.

Nothing complex so far. Assuming the header file is properly written, I can use it from C++ as usual:

void do_something()
    auto * api = open_my_api();
    // do things using api
Using the feature-packed library.

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 RAII safety goodness that unique_ptr usually provides us. But wait, api is a pointer to some allocated memory right? Why don't we use unique_ptr?

void do_something()
    auto api = std::unique_ptr<external_api>(open_my_api());
    cout <<"    -> pointer size is " <<sizeof(api) <<endl;
    // do things using api
$ ./attempt1
External C api 0x56365fe89e70 opened
    -> pointer size is 8
Segmentation fault
Initial attempt at using unique_ptr — not quite what we expected.

The issue of course, is unique_ptr is not aware of close_my_api() and attempts to clear api using delete. It may or may not crash on your system, depending on whether delete relies on free under the hood. In any case, it will never invoke close_my_api().

But the template library has a solution: we can provide a custom deleter.

Failed attempts

Working naive approach

Indeed, the full signature of unique_ptr reads like this:

template<class T,
         class Deleter = std::default_delete<T>>
class unique_ptr;
Template signature of unique_ptr.

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:

void do_something()
    using Deleter = decltype(&close_my_api);
    auto api = std::unique_ptr<external_api, Deleter>(
        open_my_api(), close_my_api
    cout <<"    -> pointer size is " <<sizeof(api) <<endl;
    // do things using api
$ ./attempt2
External C api 0x55f0667dee70 opened
    -> pointer size is 16
External C api 0x55f0667dee70 closed
Using a custom deleter with unique_ptr.

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 close_my_api() in every unique_ptr. Which is a complete waste, since we know for sure all external_api objects will always be deleted with close_my_api().

This is C++. We want zero-cost. Surely there must be a better way?

Hijacking default_delete

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?

template <> struct std::default_delete<external_api> {
    void operator()(external_api * ptr) { close_my_api(ptr); }

void do_something()
    auto api = std::unique_ptr<external_api>(open_my_api());
    cout <<"    -> pointer size is " <<sizeof(api) <<endl;
    // do things using api
This is an illegal specialization of std::default_delete. Do not try this at home.

It would seem to work, except this is illegal. Although specializing default_delete 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, 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 default_delete::operator(), 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.

Zero-cost deleter

As the existence of default_delete hints, the Deleter argument of unique_ptr actually allows function objects. That is, objects that behave as functions (supporting operator()).

This is exaclty what we will do. We use an empty object with just operator():

struct external_api_deleter {
    void operator()(external_api * ptr) const { close_my_api(ptr); }
using api_ptr = std::unique_ptr<external_api, external_api_deleter>;

void do_something()
    auto api = api_ptr(open_my_api());
    cout <<"    -> pointer size is " <<sizeof(api) <<endl;
    // do things using api
$ ./correct
External C api 0x561c3a3fce70 opened
    -> pointer size is 8
External C api 0x561c3a3fce70 closed
Custom deleter as a default-constructible functor.


Our unique_ptr calls 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 at all.2

There we have it. Legal, correct, zero-cost deleter for unique_ptr.

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!

  1. In addition, such a unique_ptr cannot 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 operator[].

  2. This is not guaranteed, but true in practice on all systems I tested this one.