I am using SDL2 for window management and rendering, but it can be a little verbose. And because of how SDL2 works under the hood, the SDL_Renderer is tied to image resource loading.
So, I am setting out to write a wrapper around the SDL initialization and window/renderer management.
The core of it is called SDLContext
, which is responsible for initializing SDL, as well as being where the SDL_Window and SDL_Renderer will reside.
It is initialized with a helper struct, SDLOpts
, which just holds any initialization values.
SDLOpts
struct SDLOpts {
int flags = SDL_INIT_EVERYTHING;
struct SDLWindowOpts {
int flags = 0;
int w = 600;
int h = 480;
} window_opts;
struct SDLRendererOpts {
int flags = 0;
} renderer_opts;
};
SDLContext.h
struct SDL_Window;
struct SDL_Renderer;
class SDLContext {
public:
explicit SDLContext(SDLOpts const& options) noexcept;
~SDLContext() noexcept;
SDLWindow sdlWindow() const noexcept;
SDLRenderer sdlRenderer() const noexcept;
private:
SDLOpts options_ {};
SDL_Window* sdlwindow_ {nullptr};
SDL_Renderer* sdlrenderer_ {nullptr};
};
SDLContext.cpp
struct SDL_Window;
struct SDL_Renderer;
class SDLContext {
public:
explicit SDLContext(SDLOpts const& options) noexcept;
~SDLContext() noexcept;
SDLWindow sdlWindow() const noexcept;
SDLRenderer sdlRenderer() const noexcept;
private:
SDLOpts options_ {};
SDL_Window* sdlwindow_ {nullptr};
SDL_Renderer* sdlrenderer_ {nullptr};
};
One thing I am on the fence about is throwing in the constructor marked noexcept.
I don't want program execution to continue if I can't initialize SDL/create a window/create a renderer, and I also want to avoid using an Init
function (trying to make it fit into RAII). But, I don't see any other alternatives.
SDLWindow and SDLRenderer are pretty bare-boned right now. They just exist as a wrapper around all of the SDL_* functions that relate to SDL_Window or SDL_Renderer.
One decision I did make was to make their default constructor private, and mark SDLContext as a friend class. This way, the only way to create an SDLWindow or SDLRenderer is from within the SDLContext class. They should not be created outside of it, because there should only be one window and one renderer.
SDLRenderer.h
struct SDL_Renderer;
class SDLRenderer {
friend class SDLContext;
public:
~SDLRenderer() = default;
//...
//Functions that draw or change the rendering properties (IE, color)
//...
private:
explicit SDLRenderer(SDL_Renderer* renderer) noexcept;
std::experimental::observer_ptr<SDL_Renderer> sdlrenderer_ {nullptr};
};
SDLWindow.h
struct SDL_Window;
class SDLWindow {
friend class SDLContext;
public:
~SDLWindow() noexcept = default;
//...
//Functions that modify the windows width and height
//...
private:
explicit SDLWindow(SDL_Window* window) noexcept;
std::experimental::observer_ptr<SDL_Window> sdlwindow_ {nullptr};
};
Any suggestions to what I've done so far? Any alternatives to stopping program execution outside of throw
in a noexcept / using an Init
function? Is it sane to mark SDLContext
as a friend class to SDLRenderer
and SDLWindow
in order to prevent them from being created wherever?
1 Answer 1
Throwing in a noexcept
function
This is legal, see https://stackoverflow.com/questions/39763401/can-a-noexcept-function-still-call-a-function-that-throws-in-c17. However, you already have doubts about it, so that's telling you it's probably not a very proper thing to do. Compilers will also warn about this.
If you want your program to terminate when it can't create a window, I would make this explicit, and instead of using throw
, call std::terminate()
explicitly.
Define a namespace for your library
All your classes and structs have names with SDL prefixed, for good reasons. However, you can make it more explicit to the compiler that you want everything in a separate namespace. The advantage is that within that namespace, you don't have to use the prefix anymore. For example, you can write:
namespace SDL {
class Window {
friend class Context;
public:
~Window() noexcept = default;
...
};
class Context {
public:
explicit Context(Opts const &options) noexcept;
~Context() noexcept;
Window Window() const noexcept;
Renderer Renderer() const noexcept;
private:
Opts options_ {};
SDL_Window *window_ {};
SDL_Renderer *renderer_ {};
};
}
In your application, you can instantiate an Window like so:
SDL::Window myWindow;
Avoid forward declarations whenever possible
Forward declarations should only be used if really necessary. Otherwise, they are just duplicating code, with the potential to make mistakes. If you are using a SDL_Window *
in your header files, just make sure you #include <SDL.h>
in it.
Avoid friend classes
Why is SDLContext
a friend class of SDLWindow
? If it needs to access some private member variables, maybe you should add public accessor functions instead? From the code you posted I don't see any reason why a friend class is necessary. In general, it is something to be avoided, since it bypasses the usual member protection scheme.
Use of std::experimental
While we all would like to have the latest and greatest language features available, consider that your code might be compiled by others using older compilers. Using experimental C++20
features might prevent them from using your code. Furthermore, observer_ptr<>
is not doing anything really, it's just there as an annotation. It's there to convey to other users that this pointer is not owned. SO it is more useful to use this for the public API than for private member variables. But, that also brings me to:
Have classes own their own pointers to SDL2 objects
It looks a bit weird that SDLWindow
has a private constructor that takes a pointer to an SDL_Window
. Why not have SDLWindow
call SDL_CreateWindow()
itself in its constructor? The same goes for SDLRenderer
.
When you do this, then SDLContext
should no longer store pointers to SDL_Window
and SDL_Renderer
, but it should instead store a SDLWindow
and SDLRenderer
directly:
class Context {
Window window_;
Renderer renderer_;
public:
Context(...);
...
};