std::unique_ptr deleters for C APIs
Table of Contents
A natural thing to do when interacting with C APIs from C++ is to replace manual memory management with RAII wrappers.
std::unique_ptr
with a custom deleter is usually the go-to choice and in most cases it’s exactly the right tool for the job.
However, there’s a slight inefficiency in how std::unique_ptr
with custom deleters is often used for this purpose and this is the topic of this post.
In the following, let’s consider the familiar C stdio API:
typedef struct some_opaque_struct FILE;
FILE* fopen(const char* filename, const char* mode);
int fclose(FILE* stream);
int fflush(FILE* stream);
Dynamic std::unique_ptr
deleter #
Now one could use std::unique_ptr
to make using the C API safer as follows.
For reasons that might be obvious to some, but will become clear later, I’ll call this usage here the dynamic deleter.
using dynamic_ptr = std::unique_ptr<FILE, int(*)(FILE*)>;
...
void foo() {
auto f = dynamic_ptr(fopen("...", "r"), fclose);
...
// No need for manual cleanup
// fclose(f.get()) will be called by f's destructor
}
This is all well and good, however there’s a slightly better way to use std::unique_ptr
in this scenario.
Static std::unique_ptr
deleter #
The alternative is to declare a deleter struct, whose operator()
just forwards to fclose
:
struct file_deleter {
void operator()(FILE* ptr) { fclose(ptr); }
};
using static_ptr = std::unique_ptr<FILE, file_deleter>;
...
void bar() {
auto f = static_ptr(fopen("...", "r"));
// ...
// No need for manual cleanup
// fclose(f.get()) will be called by f's destructor
}
Advantages #
There are at least three advantages to the second version:
Convenience #
The most obvious advantage is that the deleter function does not need to be specified when creating a static_ptr
:
auto d = dynamic_ptr(fopen("...", "r"), fclose);
auto s = static_ptr(fopen("...", "r"));
Smaller size #
The actual value of the deleter pointer of the dynamic_ptr
is only determined at runtime when it is created.
Therefore it must carry around the pointer to the deleter, making its size twice as large as the static_ptr
, where the deleter function is fixed at compile time:
static_assert(sizeof(dynamic_ptr) == 2 * sizeof(void*));
static_assert(sizeof(static_ptr) == sizeof(void*));
Less potential for misuse #
It’s possible to pass any function with the correct signature to dynamic_ptr
, including functions that don’t free the FILE
pointer:
auto a = dynamic_ptr(fopen("...", "r"), fflush); // memory leak
It’s even possible to pass a nullptr:
auto b = dynamic_ptr(fopen("...", "r"), nullptr); // boom (later)
Both of these are impossible with static_ptr
since its deleter can only be default constructed.
auto a = static_ptr(fopen("...", "r"), fflush); // compile error
auto b = static_ptr(fopen("...", "r"), nullptr); // compile error
Simplified version using C++17 #
With C++17 non-type template parameters using auto, the following helper function is possible, making the using declarations a bit simpler:
// Helper to make compile-time constant from function pointer
template <auto fn>
using make_deleter =
std::integral_constant<std::decay_t<decltype(fn)>, fn>;
using static_ptr2 = std::unique_ptr<FILE, make_deleter<fclose>>;
Limitations #
This assumes that a pointer to a given C struct will always need to be freed using the same function (fclose in our example).
If the same struct needs to be freed using different functions, you cannot use this technique and must fall back to specifying the deleter function manually when creating the std::unique_ptr
(hello ffmpeg!).
Try it yourself #
Play around with this on compiler explorer
Summary #
Don’t use this:
using file_ptr = std::unique_ptr<FILE, int(*)(FILE*)>;
auto fp = file_ptr(fopen("...", "r"), fclose);
Use this instead:
struct file_deleter {
void operator()(FILE* ptr) { fclose(ptr); }
};
using file_ptr = std::unique_ptr<FILE, file_deleter>;
auto fp = file_ptr(fopen("...", "r"));