C++ 17 Transformation...

posted in Not dead...
Published December 03, 2016
Advertisement

I'm basically really bad at working on my own projects, but with the recent release of Visual Studio 2017 RC and its improved C++17 support I figured it was time to crack on again...

To that end I've spent a bit of time today updating my own basic windowing library to use C++17 features. Some of the things have been simple transforms such as converting 'typedef' to 'using', others have been more OCD satisfying;

// Thisnamespace winprops{ enum winprops_enum { fullscreen = 0, windowed };}typedef winprops::winprops_enum WindowProperties;// becomes thisenum class WindowProperties{ fullscreen = 0, windowed};The biggest change however, and the one which makes me pretty happy, was in the core message handler which hasn't been really updated since I wrote it back in 2003 or so.


The old loop looked like this;

LRESULT CALLBACK WindowMessageRouter::MsgRouter(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ // attempt to retrieve internal Window handle WinHnd wnd = ::GetWindowLongPtr(hwnd, GWLP_USERDATA); WindowMap::iterator it = s_WindowMap->find(wnd); if (it != s_WindowMap->end()) { // First see if we have a user message handler for this message UserMessageHandler userhandler; WindowMessageData msgdata; bool hasHandler = false; switch (message) { case WM_CLOSE: hasHandler = it->second->GetUserMessageHandler(winmsgs::closemsg, userhandler); msgdata.msg = winmsgs::closemsg; break; case WM_DESTROY: hasHandler = it->second->GetUserMessageHandler(winmsgs::destorymsg, userhandler); msgdata.msg = winmsgs::destorymsg; break; case WM_SIZE: hasHandler = it->second->GetUserMessageHandler(winmsgs::sizemsg, userhandler); msgdata.msg = winmsgs::sizemsg; msgdata.param1 = LOWORD(lparam); // width msgdata.param2 = HIWORD(lparam); // height break; case WM_ACTIVATE: hasHandler = it->second->GetUserMessageHandler(winmsgs::activemsg, userhandler); msgdata.msg = winmsgs::activemsg; msgdata.param1 = !HIWORD(wparam) ? true : false; break; case WM_MOVE: hasHandler = it->second->GetUserMessageHandler(winmsgs::movemsg, userhandler); msgdata.msg = winmsgs::movemsg; msgdata.param1 = LOWORD(lparam); msgdata.param2 = HIWORD(lparam); break; default: break; } if (hasHandler) { if (userhandler(wnd, msgdata)) { return TRUE; } } MessageHandler handler; hasHandler = it->second->GetMessageHandler(message, handler); if (hasHandler) { return handler(*(it->second), wparam, lparam); } } else if (message == WM_NCCREATE) { // attempt to store internal Window handle wnd = (WinHnd)((LPCREATESTRUCT)lparam)->lpCreateParams; ::SetWindowLongPtr(hwnd, GWLP_USERDATA, wnd); return TRUE; } return DefWindowProc(hwnd, message, wparam, lparam);}The code is pretty simple;
- See if we know how to handle a window we've got a message for (previous setup)
- If so then go and look for a user handler and translate message data across
- If we have a handler then execute it
- If we didn't have user handler then try a system one


The final 'else if' section deals with newly created windows and setting up the map.

So this work, and works well, the pattern is pretty common in C++ code from back in the early-2000s but it is a bit... repeaty.

The problem comes from C++ support and general 'good practise' back in the day; but life moves on so lets make some changes.

The first problem is the query setup, the function for the 'do you have a handler' looked like this;

bool Window::GetMessageHandler(oswinmsg message, MessageHandler &handler){ MessageIterator it = messagemap.find(message); bool found = it != messagemap.end(); if(found) { handler = it->second; } return found;}Not hard;
- We check to see if we have a message handler
- If we do then we store it in the supplied reference
- Then we return if we found it or not


Not bad, but it is taking us 5 lines of code (7 if you include the braces) and if you think about it we should be able to test for the existence of the handler by querying the handler object itself rather than storing, in the calling function, what is going on. Also, the handler gets default constructed on the calling side, which might be a waste too.

So what can C++17 do to help us?
Enter std::optional.

std::optional lets us return an object which is either 'null' or contains an instance of the object of a given type; later we can tell to see if it is valid (via operator bool()) before tying to use it - doesn't that sound somewhat like what was described just now?

So, with a quick refactor the message handler lookup function becomes;

std::optional Window::GetMessageHandler(oswinmsg message){ MessageIterator it = messagemap.find(message); return it != messagemap.end() ? it->second : std::optional{};}Isn't that much better?
Instead of having to pass in a thing and then return effectively two things (via the ref and the bool return) we now return one thing which either contains the handler object or a null constructed object.
(I believe if I had written this as an 'if...else' statement that the return could simply have been {} for the 'else' path but the ternary operator messes that up somewhat, at least in the VS17 RC compiler anyway.)


So, with that transform in place our handling code can now change a bit too; the simple transform at this point would be to replace that 'bool' with a direct assign to the handler object;

UserMessageHandler userhandler;WindowMessageData msgdata;switch(message){case WM_CLOSE: userhandler = it->second->GetUserMessageHandler(winmsgs::closemsg); msgdata.msg = winmsgs::closemsg; break;// ... blah blah ..But we still have a default constructed object kicking about, not to mention the second data structure for the message data (ok, so it is basically 3 ints, but still...) - so can we change this?


The answer is yes, changes can be made with the introduction of a lambda and a pair :)

The pair is the easy one to explain; when you look at the message handling code what you get is an implied coupling between the message handler and the data which goes with it, a transformed version of the original message handler data. So, instead of having the two separate we can couple them properly;

// so this...UserMessageHandler userhandler;WindowMessageData msgdata;// becomes this...using UserMessageHandlerData = std::pair;OK, so how does that help us?
Well, on its on it doesn't really however this is where the lambda enters the equation; one of the things you can do with a lambda is declare it and execute at the same type, effectively becoming an anonymous initialisation function at local scope. It is something which, I admit, didn't occur to me until I watched [url=

]Jason Turner's talk Practical Performance Practices[url] from CppCon2016.


So, with that in mind how do we make the change?
Well, the (near) final code looks like this;

auto userMessageData = [window = it->second, message, wparam, lparam]() { WindowMessageData msgdata; switch (message) { case WM_CLOSE: msgdata.msg = winmsg::closemsg; return std::make_pair(window->GetUserMessageHandler(winmsg::closemsg), msgdata ); break; case WM_DESTROY: msgdata.msg = winmsg::destorymsg; return std::make_pair(window->GetUserMessageHandler(winmsg::destorymsg), msgdata); break; case WM_SIZE: msgdata.msg = winmsg::sizemsg; msgdata.param1 = LOWORD(lparam); // width msgdata.param2 = HIWORD(lparam); // height return std::make_pair(window->GetUserMessageHandler(winmsg::sizemsg), msgdata); break; // a couple of cases missing... default: break; } return std::make_pair(std::optional{}, msgdata); }(); if (userMessageData.first){ if (userMessageData.first.value()(wnd, userMessageData.second)) { return TRUE; }}So a bit of a change, the overall function this is in is now also a bit shorter.


Basically we define a lambda which return a pair as defined before, using std::make_pair to construct our pair to return - if we don't understand the message then we simply construct a pair with two 'null' constructed types and return that instead.
Note the end of the lambda where, after the closing brace you'll find a pair of parentheses which invokes the lambda there and then, assigning the values to 'userMessageData'.

After that we simply check the 'first' item in the pair and dispatch if needs be.
So we are done right?

Well, as noted this is 'nearly' the final solution it suffers from a couple of problems;
1) Lots and lots of repeating - we have make pair all over the place and we have to specify the types in the default return statement
2) We are still default constructing that WindowMessageData type and assign values after trivial transforms.
3) That ugly call syntax... ugh...

So lets fix that!

The first has a pretty easy fix; tell the lambda what it will return so the compiler can just sort that shit out for you;

auto userMessageData = [window = it->second, message, wparam, lparam]() -> std::pair, WindowMessageData>{ switch (message) { case WM_CLOSE: return{ window->GetUserMessageHandler(winmsg::closemsg), { winmsg::closemsg, 0, 0 } }; break; case WM_DESTROY: return{ window->GetUserMessageHandler(winmsg::destroymsg), { winmsg::destroymsg, 0, 0 } }; break; case WM_SIZE: return{ window->GetUserMessageHandler(winmsg::sizemsg), { winmsg::sizemsg, LOWORD(lparam), HIWORD(lparam) } }; break; case WM_ACTIVATE: return{ window->GetUserMessageHandler(winmsg::activemsg), { winmsg::activemsg, !HIWORD(wparam) ? true : false } }; break; case WM_MOVE: return{ window->GetUserMessageHandler(winmsg::movemsg), { winmsg::movemsg, LOWORD(lparam), HIWORD(lparam) } }; break; default: break; } return{ {}, {} };}();How much shorter is that?


So, as noted the first change happens at the top; we now tell the lambda what it will be returning - the compiler can now use that information to reason about the rest of the code.

Now, because we know the type and we are using C++17 we can kiss goodbye to std::make_pair; instead we use the brace construction syntax to directly create the pair, and the data for the second object, at the return point - because the compiler knows what to return it knows what to construct and return and that goes directly in to our userMessageData variable, which has the correct type.

One of the fun side effects of this is that last line of the lambda; return { {}, {} }
Once again, because the compiler knows the type we can just tell it 'construct me a pair of two default constructed objects - you know the types, don't bother me with the details'.

And just like that all our duplication goes away and we get a nice compact message handler.
Points 1 and 2 handled :)

So what about point 3?

In this case we can take advantage of Variadic templates, std::invoke and parameter packs to create an invoking function to wrap things away;


templatebool invokeOptional(T callable, Args&&... args){ return std::invoke(callable.value(), args...);}This simple wrapper just takes the optional type (it could probably do with some form of protection to make sure it is an optional which can be invoked), extracts the value and passes it down to std::invoke to do the calling.
The variadic templates and parameter pack allows us to pass any combination of parameters down and, as long as the type held by optional can be called with it, invoke the function as we need - this means one function for both the user and system call backs;

if (userMessageData.first){ if (invokeOptional(userMessageData.first, wnd, userMessageData.second)) { return TRUE; }}auto handler = it->second->GetMessageHandler(message);if (handler){ return invokeOptional(handler, (*(it->second)), wparam, lparam);}And there we have it, much refactoring later something more C++17 than C++03 :)


Hope this little process has been helpful, feedback via the comments if you've any idea on how to improve things or questions :)

Message router code in its final(?) form;

namespace Bonsai::Windowing // an underrated new feature...{ template bool invokeOptional(T callable, Args&&... args) { static_assert(std::is_convertible >::value); return std::invoke(callable.value(), args...); } WindowMap *WindowMessageRouter::s_WindowMap; WindowMessageRouter::WindowMessageRouter(WindowMap &windowmap) { s_WindowMap = &windowmap } WindowMessageRouter::~WindowMessageRouter() { } bool WindowMessageRouter::Dispatch(void) { static MSG msg; int gmsg = 0; if (::PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } if (msg.message == WM_QUIT) return false; return true; } LRESULT CALLBACK WindowMessageRouter::MsgRouter(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { // attempt to retrieve internal Window handle WinHnd wnd = ::GetWindowLongPtr(hwnd, GWLP_USERDATA); WindowMap::iterator it = s_WindowMap->find(wnd); if (it != s_WindowMap->end()) { // First see if we have a user message handler for this message auto userMessageData = [window = it->second, message, wparam, lparam]() -> std::pair, WindowMessageData> { switch (message) { case WM_CLOSE: return{ window->GetUserMessageHandler(winmsg::closemsg), { winmsg::closemsg, 0, 0 } }; break; case WM_DESTROY: return{ window->GetUserMessageHandler(winmsg::destroymsg), { winmsg::destroymsg, 0, 0 } }; break; case WM_SIZE: return{ window->GetUserMessageHandler(winmsg::sizemsg), { winmsg::sizemsg, LOWORD(lparam), HIWORD(lparam) } }; break; case WM_ACTIVATE: return{ window->GetUserMessageHandler(winmsg::activemsg), { winmsg::activemsg, !HIWORD(wparam) ? true : false } }; break; case WM_MOVE: return{ window->GetUserMessageHandler(winmsg::movemsg), { winmsg::movemsg, LOWORD(lparam), HIWORD(lparam) } }; break; default: break; } return{ {}, {} }; }(); if (userMessageData.first) { if (invokeOptional(userMessageData.first, wnd, userMessageData.second)) { return TRUE; } } auto handler = it->second->GetMessageHandler(message); if (handler) { return invokeOptional(handler, (*(it->second)), wparam, lparam); } } else if (message == WM_NCCREATE) { // attempt to store internal Window handle wnd = (WinHnd)((LPCREATESTRUCT)lparam)->lpCreateParams; ::SetWindowLongPtr(hwnd, GWLP_USERDATA, wnd); return TRUE; } return DefWindowProc(hwnd, message, wparam, lparam); }}
(Edit: small edit.. forgot to remove the 'WindowMessageData' type from the lambda function return statements... so now it is even shorter...)

Previous Entry Trends.
5 likes 2 comments

Comments

efigenio

Thanks for your inisghts! keep up the good work!

December 03, 2016 08:23 PM
Aardvajk
Nice work. I'm still finding new uses for the 17 features that hadn't occurred to me, particularly with lambdas and { } initialiser syntax. All great fun. Varadic templates are another area I'm finding ripe with unexpected opportunities too. 17 has really revolutionised C++ for me.
December 04, 2016 07:36 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Advertisement