C++ Using custom deleters to create a wrapper to a C interface


Example

Many C interfaces such as SDL2 have their own deletion functions. This means that you cannot use smart pointers directly:

std::unique_ptr<SDL_Surface> a; // won't work, UNSAFE!

Instead, you need to define your own deleter. The examples here use the SDL_Surface structure which should be freed using the SDL_FreeSurface() function, but they should be adaptable to many other C interfaces.

The deleter must be callable with a pointer argument, and therefore can be e.g. a simple function pointer:

std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface);

Any other callable object will work, too, for example a class with an operator():

struct SurfaceDeleter {
    void operator()(SDL_Surface* surf) {
        SDL_FreeSurface(surf);
    }
};

std::unique_ptr<SDL_Surface, SurfaceDeleter> a(pointer, SurfaceDeleter{}); // safe
std::unique_ptr<SDL_Surface, SurfaceDeleter> b(pointer); // equivalent to the above
                                                         // as the deleter is value-initialized

This not only provides you with safe, zero overhead (if you use unique_ptr) automatic memory management, you also get exception safety.

Note that the deleter is part of the type for unique_ptr, and the implementation can use the empty base optimization to avoid any change in size for empty custom deleters. So while std::unique_ptr<SDL_Surface, SurfaceDeleter> and std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> solve the same problem in a similar way, the former type is still only the size of a pointer while the latter type has to hold two pointers: both the SDL_Surface* and the function pointer! When having free function custom deleters, it is preferable to wrap the function in an empty type.

In cases where reference counting is important, one could use a shared_ptr instead of an unique_ptr. The shared_ptr always stores a deleter, this erases the type of the deleter, which might be useful in APIs. The disadvantages of using shared_ptr over unique_ptr include a higher memory cost for storing the deleter and a performance cost for maintaining the reference count.

// deleter required at construction time and is part of the type
std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface);

// deleter is only required at construction time, not part of the type
std::shared_ptr<SDL_Surface> b(pointer, SDL_FreeSurface); 
C++17

With template auto, we can make it even easier to wrap our custom deleters:

template <auto DeleteFn>
struct FunctionDeleter {
    template <class T>
    void operator()(T* ptr) {
        DeleteFn(ptr);
    }
};

template <class T, auto DeleteFn>
using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>;

With which the above example is simply:

unique_ptr_deleter<SDL_Surface, SDL_FreeSurface> c(pointer);

Here, the purpose of auto is to handle all free functions, whether they return void (e.g. SDL_FreeSurface) or not (e.g. fclose).