I got into the habit of wrapping C libraries before I start working with them. Not out of principle, but because I have seen the same mistakes often enough.

C has no RAII. Every resource you create, you have to release yourself. One function returns a pointer, another takes it back to free it. Between those two calls sits all the code that can go wrong. That leads to three recurring bugs: leaks on early return, when a later call fails and the cleanup code is never reached; double-free, when a pointer gets copied by accident and destroyed twice; and unclear ownership, because the return type alone does not say who is responsible for cleanup.

SDL2 is a typical C library in this situation, and it serves as the concrete example here. std::unique_ptr with a custom deleter solves all three. The trick is connecting the SDL type to its cleanup function:

struct Deleter {
    void operator()(SDL_Window*   p) const noexcept { if (p) SDL_DestroyWindow(p);   }
    void operator()(SDL_Renderer* p) const noexcept { if (p) SDL_DestroyRenderer(p); }
    void operator()(SDL_Texture*  p) const noexcept { if (p) SDL_DestroyTexture(p);  }
    void operator()(SDL_Surface*  p) const noexcept { if (p) SDL_FreeSurface(p);     }
};

using WindowPtr   = std::unique_ptr<SDL_Window,   Deleter>;
using RendererPtr = std::unique_ptr<SDL_Renderer, Deleter>;
using TexturePtr  = std::unique_ptr<SDL_Texture,  Deleter>;
using SurfacePtr  = std::unique_ptr<SDL_Surface,  Deleter>;

One type, four resources. unique_ptr picks the right overload based on the template parameter. The overhead is zero: empty structs with operator() are eliminated via Empty Base Optimization. The noexcept makes explicit what SDL already guarantees through its C ABI, and gives the compiler room for better optimizations.

Direct construction is not quite enough, though. When SDL_CreateWindow fails, it returns nullptr and sets an internal error string. The caller has to check afterward, and SDL_GetError() must be read immediately after the failing call, because later SDL calls overwrite it. A factory function handles this:

[[nodiscard]] inline WindowPtr make_window(std::string title,
                                           uint32_t x, uint32_t y,
                                           uint32_t width, uint32_t height,
                                           uint32_t flags) {
    auto* raw = SDL_CreateWindow(title.c_str(), x, y, width, height, flags);
    if (!raw) throw SDLError("SDL_CreateWindow");
    return WindowPtr(raw);
}

Either make_window returns a valid object, or it throws. No null check at the call site, no chance of an invalid unique_ptr making it into the rest of the code.

SDL_Init and SDL_Quit are a special case: they have process-wide effect and do not fit into unique_ptr. A dedicated class is more direct:

class Subsystem {
  public:
    explicit Subsystem(uint32_t flags) {
        if (SDL_Init(flags) != 0) throw SDLError("SDL_Init");
    }
    ~Subsystem() { SDL_Quit(); }

    Subsystem(const Subsystem&)            = delete;
    Subsystem& operator=(const Subsystem&) = delete;
    Subsystem(Subsystem&&)                 = delete;
    Subsystem& operator=(Subsystem&&)      = delete;
};

Subsystem is not moveable because SDL_Quit affects the whole process. A moved-from object could call SDL_Quit again on destruction while other parts of the program are still using SDL. Deleting the move operations means there is exactly one instance in exactly one place.

The result looks like this:

int main() {
    sdl2::Subsystem sdl{SDL_INIT_VIDEO};
    auto window   = sdl2::make_window("Demo", 100, 100, 640, 480, 0);
    auto renderer = sdl2::make_renderer(window.get(), -1, SDL_RENDERER_ACCELERATED);

    // Cleanup is automatic, in the right order: renderer, then window, then SDL_Quit.
}

The order follows from the declaration order: C++ destroys local variables in reverse. That is exactly the order SDL requires, and the language guarantees it rather than relying on discipline.

The pattern is generic. Any C library that manages resources with create and destroy functions benefits from the same approach: a Deleter struct with one overload per resource type, using aliases for unique_ptr, factory functions that catch nullptr and throw immediately, and for process-wide init and cleanup, a class with deleted copy and move operations. Write it once and you can forget that SDL2 is a C library.