C++14 and SDL2: Managing Resources
- C++ SDL2
Update: The final solution in this article has been updated based on feedback from nanofortnight on Hacker News. This change, while not solving the complexity of what it means to work with C++ templates, does help significantly with the readability of this solution.
Update 2: Added a final example that shows how the general make_resource helper can be used to simplify writing higher level C++ abstractions around C libraries
Update 3: In order to offer proper RAII semantics the make_resource function now throws if the resource fails to create. Thanks to Nicolas Guillemot for sticking out the discussions with me to help me see where this could be a potential hazard.
SDL2 is a cross-platform C library that provides developers with direct access to low level resources like the audio and graphics cards on an end user’s machine. Like most C libraries that provide access to resources, the SDL2 library’s API contains matching pairs of resource creation and deletion functions. These functions allocate and deallocate system resources making them perfect opportunities to leak said resources, if particular caution is not exercised. This article will show how C++14 can be used to manage resources created from a C based library such as SDL2.
In the case of SDL2 lets look at the example of creating an SDL_Window
resource, the application window that is drawn to the screen. While we will be focusing on the SDL_Window
in this article, this example is indicative of how all other SDL2 resources are created. Namely through the use of Creator and Destructor function pairs.
SDL_Window* window = SDL_CreateWindow(
"App Name", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
// do stuff here
SDL_DestroyWindow(window);
The problem is what occurs in the “do stuff here” region. If something occurs there that causes an exception, or otherwise prevents execution of the SDL_DestroyWindow
call, then resources are going to be leaked. In the case of the appication window this is generally not a concern as it means the application is closing and the operating system will clean everything up. However, in the case of frequently created resources such as sprite textures a leak can have significant impact on performance and stability of the application.
C++11 and 14 provide a tool that expresses single ownership of a resource, unique_ptr
. When the unique_ptr is deleted, such as when its parent container is destroyed or when it goes out of scope, a custom deleter can be called instead of the delete operator. This behavior is exactly the right mix for managing a resource created by a C library. This can be used to manage the window resource from the first code example.
{
std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> window(
DL_CreateWindow(
"App Name", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0),
SDL_DestroyWindow);
// do stuff here
} // <- window is automatically any time execution leaves this scope
Here a unique_ptr is created with two template parameters that represent the type of resource that is managed, SDL_Window
, and the signature of the deleter, a void function taking a pointer to an SDL_Window
instance, void(*)(SDL_Window*)
.
SDL_CreateWindow
is called and the result is immediately passed to the unique_ptr
as the first parameter and a function pointer to SDL_DestroyWindow
is passed as the second. At this point the SDL_Window
resource is now owned by the unique_ptr instance and when it goes out of scope, or is otherwise destroyed, SDL_DestroyWindow
will be called.
Great, now that the desired behavior has been achieved, but is there a way to make this process less verbose for users? Is there a way to generalize the creation of resources owned by a unique_ptr
? As with most things in C++ the answer is yes, with another layer of indirection.
Before getting into possible solutions, it is important to try imagining what the ideal scenario might look like. In this case the goal is to create a helper function that takes the creator and destructor from a C library that creates a resource, while at the same time still being able to pass in all the necessary initialization parameters.
template<typename Creator, typename Destructor, typename... Arguments>
auto make_resource(Creator c, Destructor d, Arguments&&... args);
auto window = make_resource(SDL_CreateWindow, SDL_DestroyWindow, "App Name",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
640, 480, 0);
Notice how this helper function no longer requires typing out the obscure unique_ptr
declaration upon initialization, auto can be used to bypass the verbose unique_ptr
declaration as its type can be easily deduced by the compiler in C++14. The first and second arguments are the C library functions that will do the work, followed by an arbitrary number of arguments that will be passed to the creator. The arguments take advantage of two more modern C++ features, parameter packs and variadic templates, a type-safe way of passing an arbitrary number of arguments.
With the declaration of the make_resource
function laid out, how might its implementation look? First, the type of the resource that the Creator
will create and the Destructor
will destroy needs to be deduced. Armed with that information the unique_ptr
can be created and initialized with the argument parameter pack.
template<typename Creator, typename Destructor, typename... Arguments>
auto make_resource(Creator c, Destructor d, Arguments&&... args)
{
auto r = c(std::forward<Arguments>(args)...);
if (!r) { throw std::system_error(errno, std::generic_category()); }
return std::unique_ptr<std::decay_t<decltype(*r)>, decltype(d)>(r, d);
}
There’s a lot going on in the body of this function. First, the resource is created on line 4, and line 5 verifies it was constructed properly, throwing if not. This ensures that any resource returned by make_resource
is guaranteed to be valid. The particular error thrown is a std::system_error
that captures the C errno
, turning it into an exception.
Finally, to construct the unique_ptr
, two bits of information are needed in the form of template parameters: the type of the owned resource and the type of the destructing function. Since the resource has already been constructed, decltype can be used on the resource r
to find its type. Since the actual type is going to be a pointer, this must be dereferenced first. In some cases, the deduced type could be a T&
or T&&
, so std::decay
is used to strip off any additional qualifiers. The second template parameter, which represents the function to be called on the owned resource when it is destroyed, is simple to deduce from the given Destructor
.
By having the resource be owned by a unique_ptr
, it allows for end users to have the most flexibility on controlling the lifetime. By default, the unique_ptr
implies single ownership of the resource. However, if the resource needs to be shared by a number of owners, the ‘unique_ptr’ return value can be used to initialize a std::shared_ptr
.
std::shared_ptr<SDL_Window> window =
make_resource(SDL_CreateWindow, SDL_DestroyWindow, "App Name",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
Final Suggestions
Now that the general solution make_resource
exists, one further step can be taken for individual libraries to make working with them even easier. First, create a namespace to store all of the helper functions in, in this case the sdl2
namespace will be used. Next, for each resource creator in the library, add a helper function to wrap the call to make_resource
, thereby eliminating the need to pass the Creator
and Destructor
explicitly and instead replacing it a with an appropriately named helper function. Below shows how this might look for the SDL_Window
resource:
namespace sdl2 {
using window_ptr_t = std::unique_ptr<SDL_Window, decltype(&SDL_DestroyWindow)>;
inline window_ptr_t make_window(const char* title,
int x, int y, int w, int h, Uint32 flags) {
return make_resource(SDL_CreateWindow, SDL_DestroyWindow,
title, x, y, w, h, flags);
}
}
The make_window helper can now be used in the following way, making for a safe and efficient way to create SDL2, or any other C library based, resources.
auto window = sdl2::make_window("App Name", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
The general make_resource
helper makes writing concrete helpers much simpler than it would be by hand. Then, those helpers can be used in higher-level wrapper objects, where the use of unique_ptr would allow the rule of zero to be invoked.
class Renderer
{
window_ptr_t _window;
public:
Renderer()
: _window(make_window("App Name", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, 640, 480, 0)) {}
// Other higher level functions here.
};