I'm currently attempting to write a small engine in C++ using glfw for window creation. I want to make strong use of raii to come up with an exception-safe architecture and make the engine practically impossible to use in the wrongway. But i'm currently struggeling with some problems and i feel i like i'm missing something important thus i'm asking here, thank you all in advance!
Suppose i have a basic raii
-struct called glfw_context
that calls glfwInit()
in the ctor
and glfwTerminate()
in the dtor
. it also accepts an error-callback
and sets it before calling glfwInit()
but i'll leave such details out since i want to focus on the basic problems.
i also created a class glfw_window
that requires a const glfw_context&
in the ctor
wheras the overloaded ctor
taking the same kinds of arguments but with a glfw_context&&
is deleted
.
the idea behind this was that when the context was an rvalue
it would only exist during the ctor
call but glfwTerminate()
would be called before all windows were properly destroyed (which happens in the dtor
of the glfw_window
class).
i implemented moves
correctly for glfw_window
whereas the glfw_context
can neither be copied or moved. but the problems start here:
there is no way that i could stop a user from creating multiple glfw_context
instances. so i went with a static
member now since glfw does not expose something like glfwIsInit() which solves this issue for the scope of my framework (only the oldest instance would call glfwTerminate()
) but that still does not protect the user from writing code like this:
std::vector< glfw_window > windows;
{
// introduce explicit scope to demonstrate the problem
glfw_context context{};
windows.emplace_back( context, ... );
}
in which case the context would still not outlive the window.
is there a nice way of solving this? i do not want to put this in the documentations as a requirement (something like "the context must outlive each window" just does not seem to cut it for me).
my current approach is to use a std::shared_pointer< glfw_context >
as an argument of glfw_window
's ctor
and store it as a member. however this still does not solve my issues, since i could still make_shared< glfw_context >()
different contexts and pass them to different windows. and since only the first allocated instance would call glfwTerminate()
i could still provoce situations where the context was destroyed before all windows.
so what is the right way of approaching that kind of problem? can i build a nice architecture that does works properly here no matter how the user tries to (mis)use it?
some other thoughts of mine include a private ctor
in glfw_context
and a static
factory-methode combined with the shared_pointer
-approach but this feels much like a singleton
and i doubt that this is the best way of approaching things.
You might use a variant of singleton:
class glfw_context;
std::shared_ptr<glfw_context> CreateContext(); // Make it visible at global scope
class glfw_context
{
glfw_context() {/*Your impl*/}
glfw_context(const glfw_context&) = delete;
glfw_context& operator=(const glfw_context&) = delete;
public:
friend std::shared_ptr<glfw_context> CreateContext()
{
static std::weak_ptr<glfw_context> instance;
auto res = instance.lock();
if (res == nullptr) {
res = std::make_shared<glfw_context>();
instance = res;
}
return res;
}
/* Your impl */
};
Then as long as there is at least one "reference" to your instance, CreateContext()
returns it, else it creates a new one.
No possibilities to have 2 different instances of glfw_context