10

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.

5
  • 3
    A completely zero-overhead approach is not possible because the underlying C API prevents zero-overhead use. But a somewhat elegant solution might still be possible if the Window class has a GLFWwindow member (by value). Would this be the case? Otherwise a lookup data structure is the best approach. And yes, if inheritance is involved then virtual methods are pretty much required - still less overhead than a hash lookup.
    – amon
    Dec 19, 2020 at 11:07
  • try replacing [this](GLFWwindow*) { with [&](GLFWwindow*) { - iirc, i don't know why, but you can't capture this explicitly, but you can capture this through capturing everything (why? i have no fking idea)
    – hanshenrik
    Dec 20, 2020 at 1:04
  • 5
    1. Check out lambdas. 2. Only worry about overhead if there is an issue with overhead. Will you have a billion windows, or a user clicking close a billion times per second? Dec 20, 2020 at 8:52
  • 1
    Your description of the problem is incomplete, making it difficult to be advise you. Does Window contain a GLFWindow* data-member? Does it contain any other state? (Does it need to contain any other state?) And since I don't know GLF in general, does GLF allow adding arbitrary state to its objects (possibly by inheritance)? Dec 20, 2020 at 12:48
  • Overhead with user interaction isn't even secondary -- everything else (like UI design and API clarity) are more important. Dec 20, 2020 at 15:15

5 Answers 5

17

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()
}
7
  • 7
    This indeed is the standard solution for C-style windowing libraries - each window provided by the framework can keep some arbitrary cookie - which is most frequently used to hold the actual instance (object) pointer, and it is used in a static method to bounce through to the actual object. And I would call this no-extra-cost as the framework is reserving these 32- (or 64-) bits anyway so no storage cost and you'd have to do something to get back to your actual object anyway.
    – davidbak
    Dec 19, 2020 at 16:43
  • And while it varies from library to library, some libraries like Python do make it zero-cost. They make the getPointer function into a macro, rather than a real function, removing all of the extra cost.
    – Cort Ammon
    Dec 19, 2020 at 18:16
  • 4
    Note that you could avoid the static function by using a lambda as OP tried. glfwSetWindowCloseCallback(mHandle, [](auto window) { static_cast<Window*>(glfwGetWindowUserPointer(wHandle))->onClose(); }) for example.
    – Christoph
    Dec 19, 2020 at 19:18
  • @Christoph cool. Lambdas didn’t exist in C++ last time I used it. Dec 19, 2020 at 21:41
  • 1
    Also, in onCloseWrapper it should be Window* w = static_cast<Window*>(glfwGetWindowUserPointer(wHandle)); w->onClose();
    – andreasxp
    Dec 20, 2020 at 12:29
14

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.

6
  • 7
    Ok, the OP made the error of giving their question a misleading title. But when we put this aside for a moment, I don't see how this this answers the actual question (which is how to avoid an overly complicated approach for the described problem)?
    – Doc Brown
    Dec 19, 2020 at 13:39
  • 1
    You're right about zero overhead meaning features don't cost if you don't use them. And its a darn good thing too! Because otherwise, if features also didn't cost if you did use them how would I ever get paid well for writing good clean efficient software! All C++ programs would run in zero time no matter how badly written!
    – davidbak
    Dec 19, 2020 at 15:52
  • 22
    The zero-overhead principle is two-pronged: the first part is ‘you don’t pay for what you don’t use’ as you say, but the other is ‘if you do use it, you couldn’t have done it better by hand’ (this is how Herb Sutter uses the term for example, so it’s not just my personal opinion). The asker is clearly referring to the second part. Dec 19, 2020 at 21:06
  • 1
    Also, the talk I linked points out how exceptions are actually in violation of the zero-overhead principle, both parts of it. Dec 20, 2020 at 9:12
  • 1
    @gnasher729: what makes you think the OP knew already, for example, what James McLeod suggested?
    – Doc Brown
    Dec 20, 2020 at 10:53
1

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.
1

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.

0

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.

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