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 Answers 5
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()
}
-
7This 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– davidbak12/19/2020 16:43:04Commented 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– Cort Ammon12/19/2020 18:16:41Commented Dec 19, 2020 at 18:16
-
4Note 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– Christoph12/19/2020 19:18:48Commented Dec 19, 2020 at 19:18 -
@Christoph cool. Lambdas didn’t exist in C++ last time I used it.James McLeod– James McLeod12/19/2020 21:41:51Commented Dec 19, 2020 at 21:41
-
1Also, in
onCloseWrapper
it should beWindow* w = static_cast<Window*>(glfwGetWindowUserPointer(wHandle)); w->onClose();
user381469– user38146912/20/2020 12:29:40Commented Dec 20, 2020 at 12:29
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.
-
7Ok, 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– Doc Brown12/19/2020 13:39:29Commented Dec 19, 2020 at 13:39
-
1You'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– davidbak12/19/2020 15:52:36Commented Dec 19, 2020 at 15:52
-
22The 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.user3840170– user384017012/19/2020 21:06:08Commented Dec 19, 2020 at 21:06
-
1Also, the talk I linked points out how exceptions are actually in violation of the zero-overhead principle, both parts of it.user3840170– user384017012/20/2020 09:12:06Commented Dec 20, 2020 at 9:12
-
1@gnasher729: what makes you think the OP knew already, for example, what James McLeod suggested?Doc Brown– Doc Brown12/20/2020 10:53:52Commented Dec 20, 2020 at 10:53
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 astd::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*>
withstd::unordered_map<GLFWwindow*, std::reference_wrapper<Window>>
; that way you would not be manipulating rawWindow
pointers. - In
makeInstance()
, I usedstd::list::emplace_back()
and notstd::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()
returnsvoid
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 bestatic 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.
[this](GLFWwindow*) {
with[&](GLFWwindow*) {
- iirc, i don't know why, but you can't capturethis
explicitly, but you can capturethis
through capturing everything (why? i have no fking idea)Window
contain aGLFWindow*
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)?