6

In my program I've included the header for an external library (GLFW) in my Main class. Everything I need to use that library for can be handled in my main class, with the exception of two sneaky little methods that I need in a separate class.

#include <GLFW/glfw3.h>
#include "graphics/Display.hpp"

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Adventum", nullptr, nullptr);

    uint32_t extCount;
    const char** extensions = glfwGetRequiredInstanceExtensions(&extCount);

    Display* display = new Display();

    //@formatter:off
    auto terminate = [&](){glfwSetWindowShouldClose(window, true);};
    auto surfaceCreation = [&](VkSurfaceKHR* surface){return (glfwCreateWindowSurface(display->instance, window, nullptr, surface));};
    //@formatter:on

    display->setTerminateFunction(terminate);
    display->setSurfaceCreationFunction(surfaceCreation);
    display->create(extensions, extCount);

    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
    }

    display->destroy();
    delete display;

    glfwDestroyWindow(window);
    glfwTerminate();
}

This is my main function. The two functions I need are glfwSetWindowShouldClose and glfwCreateWindowSurface with both requiring a reference to variables in main and would be an additional hurdle on top of including the header in both classes. As you can see I solved this by creating two lambda functions (terminate and surfaceCreation) that contain the external function call.

My question is, does this come off as an eyesore to experienced developers? Is this a crude and unnecessary work around? (I'm trying to figure out how to ask this without it being "opinion based".)

2
  • 1
    Caveat: I haven't used C++ in years. I think passing closures should be an acceptable practice, at least it is in many other languages. I'm not sure if avoiding an include is a good motivation, however I believes this results in less coupling, which is arguably desirable. By the way... it appears that display is not initialized in your code.
    – Theraot
    Jan 19 '20 at 1:04
  • @Theraot ah, it appears that its not initialized here because I was in the middle of editing my code when I copied it over. Also, not only am I avoiding an extra include but I am also avoiding the need to pass the variable window from my main class to the Display class Jan 19 '20 at 2:02
5

Preface. Much of this post is for "using C++ lambda as closures to reduce dependency and coupling" in general; therefore it contains advice for such code in general. The sample code here seems to satisfy all advice given here.


Using a C++ lambda as a closure in this case is totally acceptable. It is recommended too, since it reduces coupling and header dependency.

With the following caveats. These are "knowledge caveats", which means that you need to master them first; after that, you can use C++ lambda without fear.

  1. Deep understanding of C++ copying vs. referencing.
  2. Deep understanding of C++ scope and lifetime.
  3. Understand how (1) and (2) (copying, referencing, scope, lifetime) applies to C++ lambda: for the captured variables, and for the lambda itself.
  4. Able to create a C++ lambda that has the correct lifetime characteristics.
  5. Able to write functions and methods that properly receives a C++ lambda input argument with the intended lifetime characteristics, by using input signature and specifier correctly. Usually, the delegatee receives the C++ lambda (and possibly stores it) as a std::function.

One maintenance caveat.

  1. While C++ lambda (closure) allows foreign code to schedule task to be executed without having to know what's inside, it does require the mutual understanding - between the "delegator" (code that instantiates and gives out a C++ lambda) and the "delegatee" (code that receives a C++ lambda and execute it later) - about the ordering of execution, in relation to other pieces of code.

Rule-of-thumb about execution ordering dependency:

  1. Usually, the ordering of code for settings things up is easy. The dependency is both natural and explicit. (a later step will likely require values and objects created in an earlier step)

  2. However, the ordering of code for taking things down (destruction) is not easy. Very often, things will happen in a different order than the programmer has intended.

    • Tips: If the destruction ordering is critical to correctness, use logging to make sure the steps happen in the correct order.

Applying these knowledge to your code sample:

  • First of all, stay calm. Remember that this is the int main() method, therefore most local variables created in this method will indeed last almost as long as the application's runtime. However, if there's any piece of code that execute after the main method terminates (for example, destructors of static objects), that may trigger a use-after-free condition.

  • Understand that, in the first lambda (terminate), the local variable window is captured by reference. Even though window is a pointer, the use of capture-by-reference ([&](){...}) means that the address of the local variable that stores this pointer is being captured.

  • If the pointer value is meant to be copied, use capture-by-copy ([=](){...}), or specify the variable by name ([window](){...}).

  • If the variable holding the pointer value is meant to be accessed by delegatee (highly unlikely, unless the delegatee needs to update it to point to a different object), use capture-by-reference.

  • If there is a need to extend the lifetime of the object being captured, use one of the C++ smart pointers: typically shared_ptr or weak_ptr; rarely unique_ptr (see note). This is useful if the delegatee is intended to live longer than the delegator. (This is not the case here, since the main method will live longer than any delegatee in this sample code.)

  • Note about capturing unique_ptr. In order to properly transfer a unique_ptr from the delegator to the lambda (the "task / closure"), C++14 is required. This uses lambda capture expressions. (Most people who aren't free to choose C++14 over C++11 will avoid the issue by staying with shared_ptr or weak_ptr most of the time.)

1
  • So if I understand this correctly, it isn't necessary to capture a pointer by reference because a copy of a pointer will still point to the same memory address? Jan 19 '20 at 4:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.