Jump to content

  • Log In with Google      Sign In   
  • Create Account

Not dead...

Featured

C++ 17 Transformation...

Posted by , in C++, C++17 03 December 2016 - * * * * - · 1,144 views

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;

// This

namespace winprops
{
    enum winprops_enum
    {
        fullscreen = 0,
        windowed
    };
}
typedef winprops::winprops_enum WindowProperties;

// becomes this
enum 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<T>.

 

std::optional<T> 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<MessageHandler>  Window::GetMessageHandler(oswinmsg message)
{
    MessageIterator it = messagemap.find(message);
    return it != messagemap.end() ? it->second : std::optional<MessageHandler>{};
}
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<UserMessageHandler, WindowMessageData>;
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=https://youtu.be/uzF4u9KgUWI]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<UserMessageHandler>{}, 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<std::optional<UserMessageHandler>, 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;

 


template<typename T, typename... Args>
bool 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<typename T, typename... Args>
	bool invokeOptional(T callable, Args&&... args)
	{
		static_assert(std::is_convertible<T, std::optional<T::value_type> >::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<std::optional<UserMessageHandler>, 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...)




Trends.

Posted by , 05 March 2013 - - - - - - · 1,303 views

With the launch of SimCity I have noticed an interesting trend developing; the acceptance that the launch of any game will be a week of frustration and disconnections while the publisher sorts out the servers.

Note I said 'any game'.
Not a multi-player game.
Not an MMO.
Any game.

One fan of SimCity, when faced with the question of 'why cant I play what is largely a single player game because I can't connect to the servers?' responded by comparing it to an MMO launch and that 'this should be accepted'.

In fact I'm getting a sense of deja-vu as I sit here and write this as I'm sure I've called this subject out before?

SimCity might well have multi-player aspects which require a connection but when the game has the ability to mark a region as 'private' this implies you can play on your own which brings up the question of why do I need to be online to do this and why can't you play the new game at release, instead having to suffer a week of 'server issues' while the publisher waits for demand to drop off rather than deal with it directly.

This acceptance I find worrying because it is a slide towards a world where you install your shiny new single player game but instead of being able to play it you are forced to login to a server which will not have enough capacity to deal with the launch day demand because the publish didn't want to spend the cash to do so.

Note this is not an argument against 'online drm' - my acceptance of Steam pretty much gives me very little to stand on there. This is against the requirement to be connected to experience a product when the person you have brought it from clearly hasn't, and never had plans to, allow everyone to experience it one day one.

In this instance given the overly inflated prices of games on Origin this is pretty unacceptable.
(I'll refrain from a longer anti-Origin rant at this point however.)

But I guess while people will pay the money for a game which may or may not work on release (and more importantly KEEP paying) this is a trend unlikely to reverse.

The funny thing is I dare say a cross section of this crowd have also complained about the idea of the next consoles requiring an online connection...


"Valve Box"

Posted by , 10 December 2012 - - - - - - · 1,223 views

Valve: "Windows 8 App store is bad!... btw you can now buy apps from our store!"

Valve: "Closed systems are bad! btw, here is our new closed system"

Gamers : "OMG YOU ARE SO RIGHT AND THIS ISNT AT ALL A CONTRADICTION HERE HAVE MY MONEY!"

*facepalm*


Conclusion 2.

Posted by , 10 November 2012 - - - - - - · 1,184 views

The person who posted the first of the 'tutorials' we see on line these days has a lot to answer for.

While it is creating more "programmers" (and I use the phrase loosely) this reliance on tutorials with snippets of code and even video tutorials showing you everything is, imo, having a bad effect on the ability of those who follow them to problem solve.

Instead of learning to read docs, read books and figure out samples they instead require a step by step guide on the most trivial of things and then complain when such resources don't exist.

On the plus side as this army of vague competence marches forward at least there will always be better paid work for those of us who can think our way out of a paper bag instead of sitting at the bottom of it and crying because no one has made a video showing us how to get out.


Conclusion.

Posted by , 09 November 2012 - - - - - - · 927 views

In all my years of programming, both professionally and as a hobby, there is one truth I've learnt over the years which stands above all overs.

Most people can't design software for shit.

This thought depresses me.






Recent Entries

Recent Comments