Question

As an exercise in code architecture, I was writing a C++ wrapper for a window library called GLFW3.

In this library, when a window's X button is pressed, you can register a callback that reacts to such event. The callback needs to look like this:

void callback_name(GLFWwindow* handle);

That is, it needs to be a pointer to function that returns nothing and takes a pointer to the window handle.

Now, in my wrapper a window is a proper class called Window. And I would like to transform the GLFW-approach of using callbacks to using an handling function Window::onClose akin to what Qt does. For example, the default onClose function would look like this:

class Window {
    ...
    void onClose() {
        hide(); // remove the window from view and from taskbar
    }
};

The problem

The problem arises when I try to make this work without introducing overhead into the wrapper. The naïve solution is to register the callback in the constructor:

// DOES NOT COMPILE
class Window {
    ...
    Window(...) {
        glfwSetWindowCloseCallback(mHandle, 
            [this](GLFWwindow*) { // When X button is pressed, call this onClose function
                onClose();
        });
    }

This does not compile, because the "callback" needs to capture this pointer to call a member function, and glfwSetWindowCloseCallback expects a raw function pointer.

Getting into more complex solutions, one could maintain a map such as std::unordered_map<GLFWwindow*, Window*> handle_to_window, and do the following:

glfwSetWindowCloseCallback(mHandle, 
    [](GLFWwindow* handle) {
        handle_to_window[handle]->onClose();
    });

BUT this would break with inherited classes unless I make the onClose function virtual (which would introduce overhead).

Question

My question is, is there a way to make this work without introducing more overhead? Answers that change the architecture of the wrapper are fair game also.

Was it helpful?

Solution

It's not quite zero-overhead, but you can use the GLFWwindow User Pointer Property to make a relatively clean solution, I think (caveat: my C++ is a little rusty, and I am not familiar with the GLFW library).

In your Window constructor,

Window(...) {
        // ...
        glfwSetWindowUserPointer(mHandle, static_cast<void *>(this));
        glfwSetWindowCloseCallback(mHandle, Window::onCloseWrapper);
};

And then define onCloseWrapper (in the Window class) as:

static void onCloseWrapper(GLFWwindow *wHandle) {
        Window *w = static_cast<Window*>(glfwGetWindowUserPointer(wHandle));
        w->onClose()
}

OTHER TIPS

You misunderstood the “zero overhead” principles. It means that features of the C++ language should have zero overhead if you don’t use the feature. For example, exceptions have overhead if you use them, and that’s fine, but the fact alone that the language supports exceptions causes no runtime overhead if you don’t use them.

Proposed solution

I propose a bunch of static "dispatcher" functions, which would lookup the right Window instance and then forward the received GLFWwindow* to that instance, so it could do the real work:

class Window {
    // // // Part 1: object-oriented UI stuff // // //
    float _coolnessFactor; /* your data members */
    Window (/*params?*/) { /*initialize data members*/ }
    void onClose (GLFWwindow* p) { /*your UI code goes here*/ }
    void onMinimize (GLFWwindow* p) { /*your UI code goes here*/ }
    // Etc.

    static std::list<Window> _instances; /*actual Window objects themselves*/
    // // // Part 2: handle-to-instance lookup and dispatch // // //
    static std::unordered_map<GLFWwindow*, Window*> _handles; /*Window pointers
                              point to the actual Window objects in _instances */

    static Window* lookupInstance (const GLFWwindow* p) {
        decltype(_handles)::const_iterator it = _handles.find(p);
        if (it == _handles.cend()) { /*Bug! print some diagnostics and quit, maybe?*/ }
        return it->second;
    } /*Helper method, shared by the dispatch_onXYZ() functions. */
    
    static void dispatch_onClose (GLFWwindow* p) {
        static const void (Window::*pointerToMember)(GLFWwindow*) = &Window::onClose;
        Window *pw = lookupInstance(p);
        (pw->*pointerToMember)(p); /*this is the dispatch!*/
    }
    static void dispatch_onMinimize (GLFWwindow* p) {
        static const void (Window::*pointerToMember)(GLFWwindow*) = &Window::onMinimize;
        Window *pw = lookupInstance(p);
        (pw->*pointerToMember)(p); /*this is the dispatch!*/
    }
    // Etc.; one dispatch_onXYZ() static method for every onXYZ() member method.

public:
    // // // Part 3: register the dispatchers // // //
    static void initialize ( ) {
        glfwSetWindowCloseCallback( ((*)(GLFWwindow*)) Window::dispatch_onClose);
        glfwSetWindowMinimizeCallback( ((*)(GLFWwindow*)) Window::dispatch_onMinimize);
        // Etc.
    }

    // // // Part 4: factory method // // //
    static Window& makeInstance ( ) {
        const GLFWwindow *p = /*obtain from somewhere, I don't know where*/;
        Window& w = _instances.emplace_back(/*constructor args*/);
        (void) _handles.insert({p,&w});
        return w;
    }
};

; so then you would

int main ( ) {
    Window::initialize();
    /* some code */
    Window& wPopup = Window::makeInstance();
    /* some more code */
    Window& wDialog = Window::makeInstance();
}

Notes

  • This solution lets you have Window objects, to program UI stuff in an object-oriented style: that's what you want to do, if I understood your question correctly?
  • Chose std::list, because pointers or references to existing elements are never invalidated when a new element is added to a std::list.
  • When registering the dispatch_onXYZ() callbacks, I cast their addresses to (*)(GLFWwindow*), so the registration functions don't get confused; I'm not certain that such a cast will be required in your case, but it might be.
  • You can replace std::unordered_map<GLFWwindow*, Window*> with std::unordered_map<GLFWwindow*, std::reference_wrapper<Window>>; that way you would not be manipulating raw Window pointers.
  • In makeInstance(), I used std::list::emplace_back() and not std::list::push_back(), out of respect for your focus on lowering overhead: emplace_back() avoids the cost of a copy assignment or move assignment.
  • std::list::emplace_back() returns void until C++17, so if you're working with C++11 or C++14 you would need an extra line of code there.
  • If implementing this for real, you would probably want to make lookupInstance(), at least, threadsafe.
  • Dispatch via pointer to member has near-zero overhead; as you see, pointerToMember can be static const, because it is always same.

How far down the rabbit hole do you want to go?

The C++ virtual function table based dynamic dispatch is just one OO model. You can implement others, and your code is still C++.

Suppose you want to have a polymorphic method without vtable overhead.

template<class T> using ptr_to = T*;

template<auto F, class T>
struct Operation {
  constexpr operator ptr_to<R(*)(void*, Args...)>()const {
    return [](void* ptr, Args...args)->R{
      return std::invoke(F, static_cast<T*>(ptr), std::forward<Args>(args)...));
    };
  }
};

This works like this:

struct Base {};
struct Bob:Base {
  void bark() { std::cout << "woof\n"; }
};
struct Alice:Base {
  void bark() { std::cout << "woof woof\n"; }
};

void call( void(*fun)(void*), void* state ) {
  fun(state);
}

Alice a;
Bob b;
call( Operation<&Bob::bark, Bob>{}, &b );
call( Operation<&Alice::bark, Alice>{}, &a );

no vtable.

Now, in your case, you need to go from GLFWindow to your object. Store the object as a void* inside GLFWindow. Modify Operation to go from the GLFWindow* to the void* object before casting to T. Then invoke.

To install the callbacks, use CRTP.

template<class Derived>
struct MyWindow {
  MyWindow( GLFWindow* rawWindow ) {
    glfwSetWindowUserPointer(rawWindow, this);
    glfwSetWindowCloseCallback(rawWindow, Callback<&Derived::onClose, Derived>{});
  }
}; 

or, explicitly:

template<class Derived>
struct MyWindow {
  MyWindow( GLFWindow* rawWindow ) {
    glfwSetWindowUserPointer(rawWindow, static_cast<Derived*>(this));
    glfwSetWindowCloseCallback(rawWindow, +[](GLFWindow*ptr) {
      void* pvoid = glfwGetWindowUserPointer(ptr);
      static_cast<Derived*>(pvoid)->onClose();
    });
  }
}; 

now your BobWindow looks like:

struct BobWindow:MyWindow<BobWindow> {
  void onClose() { /* code */ }
}

you are outsourcing your vtable to CRTP and to function pointers in the GLFWindow object.

If you know for a fact that your callbacks will be driven only for your windows, you don't need to pointer-chase, the overhead can be reduced to zero or a branch-folding-error on same:

struct W { int GLFWstuff; };
struct C : W { int mystuff; void close_requested(); };
void W_close_requested(struct W *ptr) {
    static_cast<C*>(ptr)->close_requested();
}

compiles to a single jump at -Os or above, and if the method definition is available it will likely just inline it.

But this is at best skirting root-of-all-evil territory, and for code being called once in response to actual I/O (let alone freakin' human muscular action) even reducing the overhead to actual zero is going to have no measurable benefit.

Cycle counting once-per-user-action responses is a waste. The time spent to type the first character of your question title exceeds all the cpu time spent across the lifetime of the universe by any halfway-reasonable implementation.

And if you might, ever, encounter callbacks for objects created by e.g. a 3rd party mod or say a debugging interface you bolted on later, then you're in undefined territory. So there's that.

But the downcast option is useful when you do need it, say if you're talking to a library and processing a bazillion objects you know you made, so I thought I'd mention it here.

Licensed under: CC-BY-SA with attribution
scroll top