Search code examples
c++glfwraiiexception-safetyc++17

raii architecture using c++ and glfw


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.


Solution

  • 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