You don’t need a stateful deleter in your unique_ptr (usually)

This is my first English post in a long while, I hope I am not too rusty.

With the increasing probability of your average project using at least C++11, std::unique_ptr is one of the most popular solutions1 to dynamically managing memory in C++.

It usually goes like this: if your object can’t be a scoped object, that is one with automatic lifetime (colloquially: on the stack, or a class’s member), wrap it in std::unique_ptr. This will ensure it’s freed when it finally goes out of scope. Herb Sutter goes more in-depth into this topic in his last C++Con talk:

Herb Sutter: Leak-Freedom in C++… By Default.

This advice is sound and deserves being spread.

Custom deleters

There is, however, a slight problem with how this gets expanded when it comes to using std::unique_ptr to guard lifetime of types requiring a custom deleter. Very often the resource held has its own deallocation function, be it fclose for FILE, FreeLibrary for WinAPI’s HMODULE, or something else entirely.

Despite the function being known at compile time, the vast majority of examples (to name a few: the first google result for “how to use unique_ptr custom deleter” and the original Rule of Zero article) suggest using a function pointer as the deleter type and supplying the specific (constant!) function pointer at runtime:

std::unique_ptr<FILE, int(*)(FILE*)> f{fopen("foo.txt", "r"), &fclose};

Or, as a class member:

struct something_using_a_file
{
    something_using_a_file(char const* name):
        file_{fopen(name, "r"), &fclose}
    {
    // stuff
    }
 
    // more stuff
 
    std::unique_ptr<FILE, int(*)(FILE*)> file_;
};

Not default constructible

The current standard draft states the following about unique_ptr‘s default constructor:

§23.11.1.2.1 [unique.ptr.single.ctor] / 4
Remarks: If is_­pointer_­v<deleter_­type> is true or is_­default_­constructible_­v<deleter_­type> is false, this constructor shall not participate in overload resolution.

It is not an empty statement either – all the standard libraries of major compilers implement this: clang, gcc, msvc. The following code does not compile with any of the aforementioned:

#include <memory>
 
int main()
{
    std::unique_ptr<int, void(*)(int*)> empty;
}

In many cases, this characteristic makes std::unique_ptr with function pointer deleter unusable in containers: an std::array‘s elements must all be fully initialized, std::vector‘s resize() is unavailable, and so is std::map‘s operator[].

using evil = std::unique_ptr<FILE, int(*)(FILE*)>;
 
std::array<evil, 1> x{{ {fopen("foo.txt", "r"), &fclose} }};
std::array<evil, 2> y{{ {fopen("bar.txt", "r"), &fclose} }}; // error
using evil = std::unique_ptr<FILE, int(*)(FILE*)>;
 
std::vector<evil> x(1); // error
std::vector<evil> y;
y.resize(1); // error
using evil = std::unique_ptr<FILE, int(*)(FILE*)>;
 
std::map<int, evil> x;
x[42]; // error

Disabling useful functionality – for no good reason – alone should be a deal-breaker, but there’s more:

Efficiency

On all architectures readily available to me2, the following assertion did not fire:

using standard = std::unique_ptr<int>;
using custom = std::unique_ptr<int, void(*)(int*)>;
static_assert(sizeof(standard) * 2 == sizeof(custom), "");

The explanation for this behaviour should be fairly obvious: after all, the function pointer is additional state and – in theory – it could be anything. That being said, the 2× ratio is not defined, although it is a safe assumption on any modern architecture I can think of.

What’s more, it could be changed at runtime:

int fclose_and_log(FILE* ptr)
{
    cout << "fclose(" << ptr << ")\n";
    return fclose(ptr);
}
 
int main()
{
    using evil = std::unique_ptr<FILE, int(*)(FILE*)>;
    evil ptr{fopen("foo.txt", "w"), &fclose};
    ptr = evil{fopen("bar.txt", "w"), &fclose_and_log};
}

The above example is artificial. In the vast majority of cases the function is constant and will not be changed throughout the program execution. Let’s consider yet another artificial example – a std::vector of evil, as defined below:

int main()
{
    using evil = std::unique_ptr<FILE, int(*)(FILE*)>;
 
    std::vector<evil> files;
    files.reserve(64);
    std::generate_n(std::back_inserter(files), 64,
                    [n = 1]() mutable {
        auto name = std::to_string(n++) + ".txt";
        return evil{fopen(name.c_str(), "w"), &fclose};
    });
}

On my system, the vector‘s allocated memory is laid out as follows:

memory layout of `files` (`std::vector<evil>`)Memory layout of files (std::vector<evil>)

Can you guess what the values highlighted to yellow are? Well, at this point I don’t expect there’s much guessing going on. You’re right, they’re all addresses of fclose0x408a30. Quite redundant.

While I don’t think there’s a noticeable difference in performance speed-wise[citation needed], at least as long as the size of such an array is small enough, there’s clearly a pessimization of space efficiency. Doubling the size of a pointer is in many cases a deal breaker, as it could cause even larger problems if placed within a structure next to members with high alignment requirements.

In my opinion, had the doubling of size been a true necessity, it would be an understandable reason to steer away from std::unique_ptr, especially on memory-constrained architectures.

What to do?

Fortunately, there’s no need to eschew std::unique_ptr in such cases. In fact, even one of the less upvoted answers to the StackOverflow question touches upon this: create custom deleters that do not need to keep any state:

struct FileDeleter
{
    void operator()(FILE* ptr) const {
        fclose(ptr);
    }
};

Now, the usage is very similar, but all of the above problems are absent. It is default-constructible with a sane value, so it could be said that it makes std::unique_ptr‘s constructor great again:

using good = std::unique_ptr<FILE, FileDeleter>;
 
std::array<good, 1> x{{ {fopen("foo.txt", "r")} }};
std::array<good, 2> y{{ {fopen("bar.txt", "r")} }}; // ok
using good = std::unique_ptr<FILE, FileDeleter>;
 
std::vector<good> x(1); // ok
std::vector<good> y;
y.resize(1); // ok
using good = std::unique_ptr<FILE, FileDeleter>;
 
std::map<int, good> x;
x[42]; // ok

Furthermore, any compiler worth its salt will use empty base optimization to ensure that the size of std::unique_ptr with a stateless deleter is exactly that of a pointer. This is especially unlikely to be untrue, since the standard library uses a stateless std::default_delete to call delete and delete[]. The following assertion holds true on all platforms and compilers available to me2. Nothing exotic has been tested, though.

static_assert(sizeof(FILE*) == sizeof(std::unique_ptr<FILE, FileDeleter>), "");

Finally, there’s no need to provide the secondary argument to the std::unique_ptr‘s constructor – the deleter is default-constructible.

To put all of the above to a test, let’s try again, with a std::vector of good:

struct FileDeleter
{
    void operator()(FILE* ptr) const {
        fclose(ptr);
    }
};
 
int main()
{
    using good = std::unique_ptr<FILE, FileDeleter>;
 
    std::vector<good> files;
    files.reserve(64);
    std::generate_n(std::back_inserter(files), 64,
                    [n = 1]() mutable {
        auto name = std::to_string(n++) + ".txt";
        return good{fopen(name.c_str(), "w")};
    });
}
memory layout of `files` (`std::vector<good>`)Memory layout of files (std::vector<good>)

The values highlighted yellow are addresses of fclose, in this run 0x408a18. Yes, there are none; that’s the point.

In this battle of good vs. evil, evil definitely comes off worse.

Making your life easier with templates

Some could say that writing a deleter class for every resource is quite a mouthful, although I dare anyone to tell me that having to pass function pointer to every instantiation is better. That being said, one could avoid having to define a class for each deleter, for example with the following function_caller template:

template<typename T, T* func>
struct function_caller
{
    template<typename... Us>
    auto operator()(Us&&... us) const
        -> decltype(func(std::forward<Us...>(us...)))
    {
        return func(std::forward<Us...>(us...));
    }
};

It would be used as follows and it would have all the desired characteristics of std::unique_ptr with BarDeleter:

int main()
{
    using good = unique_ptr<FILE, function_caller<int(FILE*), &fclose>>;
 
    std::vector<good> files;
    files.reserve(64);
    std::generate_n(std::back_inserter(files), 64,
                    [n = 1]() mutable {
        auto name = std::to_string(n++) + ".txt";
        return good{fopen(name.c_str(), "w")};
    });
}

Since this is virtually equivalent to the above example of good, I will not insert duplicates of the same snippets.

C++17 goodies

Finally, with C++17 you can use auto template parameters:

template<auto func>
struct function_caller
{
    template<typename... Us>
    auto operator()(Us&&... us) const
    {
        return func(std::forward<Us...>(us...));
    }
};

Then good can be defined as follows:

using good = unique_ptr<FILE, function_caller<&fclose>>;

That being said, it stops being that pretty when an overload needs to be selected.

What if I actually need state?

If you do, then you do. I’m not saying don’t use stateful deleters. Instead, I am saying that you should only use stateful deleters if you need that state, which is obviously not true for all the examples I’ve presented.

In any case, if you’re going to create a stateful deleter, consider making it default-constructible, to prevent problems described above.

Conclusion

std::unique_ptr is a great and versatile tool, well-deserving its ubiquity. Unfortunately, some internet advice regarding its more advanced usage is, at best, questionable. I hope that this post will be found helpful, especially to those disheartened by apparent deficiencies of custom deleters.

1Along with std::vector, but this is not pertinent to this post.
2Windows x86 (MinGW gcc 5.3.2), x86_64 (MSVC 13), Arch Linux x86_64 (gcc 7.1.1 and clang 4.0) andArch Linux ARMv7 on my Raspberry Pi 3 (32 bit, gcc 7.1.1 and clang 4.0).

4 thoughts on “You don’t need a stateful deleter in your unique_ptr (usually)

  1. Ola,
    Nice article ;)
    What if i wanted to use std::function as a deleter ?
    typedef deleterFn std::function ;
    std::unique_ptr<FILE, deleterFn> f{fopen(“foo.txt”, “r”), [](*File){fclose(File)}};

    1. Thanks :) It seems my default WordPress doesn’t like < and > much (I just used &lt​; and &gt​; to display them), I’ll have to fix that.

      To answer your question: if you actually need a std::function/stateful deleter, then you should use it. Your example doesn’t convince me that you do, but it may be overly simplified. std::function carries its own size and performance penalties (I’d suggest benchmarking first), but other than that might be a fine choice. You’d still need to supply the deleter constructor argument everywhere, but at least you’d get a default-constructible unique_ptr.

  2. Any thoughts on specializing std::default_deleter for FILE instead?

    namespace std {
    template void default_delete::operator()(FILE *ptr) const { fclose(ptr); }
    }

    1. As far as I know, this is not a legal specialization point, so it’s not possible. It also feels like a global solution to a local problem.

Leave a Reply

Your email address will not be published.