Perfect Messaging On C++

Published May 02, 2018
Advertisement

Messaging Design Pattern (MDP)

Components of a program need to communicate with each other. In messaging design pattern this communication is done via sending and processing messages. A third party component called messenger stands between different components and does the task of passing proper messages to proper components. MDP increases decoupling, encapsulation, and reusability by removing component communication from component functionality. (I am not discussing distributed systems here)

Implementation

This pattern is often implemented using a message processing interface. Any component that wants to work with the messenger should implement this interface. This interface declares a process_message method that processes a message and returns a message to be sent to other components.

Issues

This implementation causes the following performance issues:

  1. Messages are usually broadcasted to all components.
  2. For a component to detect whether a message should be processed or not, it should check either the type of the message or some other member of the message.
  3. Implementing interface requires inheritance and increases the function call overhead.

These performance issues make MDP unsuitable for communicating small amounts information, which puts a limitation on the ways we use the messaging system to communicate.

C++ Implementation

In my implementation of MDP I had the following concepts in mind:

  1. People use C++ to achieve zero-overhead abstraction (This is not java).
  2. Messaging should be so cheap that it should be used as the preferred means of communication.
  3. The components are available at compile time (we will utilize this to achieve to the goals above).

Solution

  1. Find the message destination by message type on compile time (eliminates broadcasting).
  2. Passing messages are function calls, try to inline them (eliminate overhead).
  3. Since messages have different types, the components' interfaces are different (i.e. each have different process_message methods), there is no need for inheritance.

Messenger

Since the components are available at the compile time, the messenger can make a tuple of the components it's going to handle. The messenger implements pass_message method as the means of passing messages to internal components from external components. Passing messages includes calling process_message of all components, this is broadcasting, but there is a solution to eliminate redundant process_message calls, described later. One more method get_module is implemented to allow access to components. Note that each component after processing a message returns a tuple of messages to be replied, this also has to be taken care of.


//messenger.inl
#pragma once

#include <cstddef>
#include <tuple>

namespace Messenger
{
	namespace Privates
	{
		//Passes a single message to a tuple of modules.
		template <std::size_t N, std::size_t M> struct message_passer;
		//Passes a tuple of messages to a tuple of modules.
		template <std::size_t N, std::size_t M> struct tuple_message_passer;
		//Passes a tuple of messages to a tuple of modules.
		template <typename MTT, typename TT> void pass_tuple_message(const MTT messages_tuple, TT& modules_tuple);

		template <std::size_t M> struct message_passer <0, M>
		{
			template <typename MT, typename TT> static void pass_message(const MT& message, TT& modules_tuple) {}
		};

		template <std::size_t N, std::size_t M> struct message_passer
		{
			//Pass a single message to a tuple of modules, the order of modules in the tuple, defines the order of the messages being passed.
			//The result of processing each message is a tuple of messages to be passed to the modules, which is done using pass_tuple_message.
			template <typename MT, typename TT> static void pass_message(const MT& message, TT& modules_tuple)
			{
				pass_tuple_message((std::get<M - N>(modules_tuple)).process_message(message), modules_tuple);
				message_passer<N - 1, M>::pass_message(message, modules_tuple);
			}
		};



		template <std::size_t M> struct tuple_message_passer <0, M>
		{
			template <typename MTT, typename TT> static void pass_message(const MTT& messages_tuple, TT& modules_tuple) {}
		};

		template <std::size_t N, std::size_t M> struct tuple_message_passer
		{
			//Pass a tuple of messages to a tuple of modules. First the first message on the tuple is passed to all of the modules then the rest of the messages are passed in the same manner.
			template <typename MTT, typename TT> static void pass_message(const MTT& messages_tuple, TT& modules_tuple)
			{
				message_passer<std::tuple_size<TT>::value, std::tuple_size<TT>::value>::pass_message(std::get<M - N>(messages_tuple), modules_tuple);
				tuple_message_passer<N - 1, M>::pass_message(messages_tuple, modules_tuple);
			}
		};


		//The helper function that passes a tuple of messages to a tuple of modules.
		template <typename MTT, typename TT> void pass_tuple_message(const MTT messages_tuple, TT& modules_tuple)
		{
			tuple_message_passer<std::tuple_size<MTT>::value, std::tuple_size<MTT>::value>::pass_message(messages_tuple, modules_tuple);
		}
	}

	//The messenger
	template <typename... Ts > class messenger
	{
		//The tuple of modules
		using module_tuple_type = std::tuple<Ts...>;
		mutable module_tuple_type modules;

	public:
		messenger() {}

		//pass a message to internal modules
		template <typename MT> void pass_message(const MT& message) const
		{
			Privates::message_passer<sizeof... (Ts), sizeof... (Ts)>::pass_message(message, modules);
		}

		//get a certain module
		template <std::size_t N> typename std::tuple_element<N, module_tuple_type>::type &get_module()
		{
			return std::get<N>(modules);
		}
	};
}

Components

Each components is a class that implements the process_message method, that takes a const reference message and returns a tuple of const reference messages. The message type will vary between different message processors, meaning a components may have several overloads of the process_message method with varying return types, and each overload only handles one type of a message.

In this method since all the messages are broadcasted to all the components, all components should process all the messages, but the goal is to eliminate redundant message processing, this is done using the following piece of code:


//messenger-module.inl
#pragma once

#include <cstddef>
#include <tuple>

//Note: Add to the end of the module class to process known messages only, other wise you need to define a message processor for each message type.
#ifndef PROCESS_KNOWN_MESSAGES_ONLY
#	define PROCESS_KNOWN_MESSAGES_ONLY public: template<typename MT> std::tuple<> process_message(const MT& message) {return std::tuple<>();}
#endif

Adding PROCESS_KNOWN_MESSAGES_ONLY to the end of each component class, defines a default message processor that does nothing and returns an empty tuple of messages. This means if no explicit message processor for a message type is defined in the components class, the message processing is done in using this default message processor which can be optimized away. Hence redundant messages are not passed to modules after optimization.

Sample component

The following module processes messages of type std::string, rest of the messages are processed by default message processor.


//test-module-b.inl
#pragma once

#include <string>
#include <iostream>

#include "messenger-module.inl"

namespace Test
{
	class module_b
	{
	public:

		std::tuple<> process_message(const std::string& message)
		{
			std::cout << message << std::endl;
			return std::tuple<>();
		}

		PROCESS_KNOWN_MESSAGES_ONLY
	};
}

One more step

So far components can pass messages, only if they have received a message. In other words they just can reply to a message, they can't pass a message without processing a message. It would be useful if such utility was present to each component to pass a message before or even while processing a message.

For a component to pass a message, the component needs access to the messenger which means that these classes will be codependent, not only each component and messenger would become codependent but all components as well would become codependent to each other. I have addressed this problem with a trick as follows, but I am not satisfied with it. Any solutions are welcome :)

Current solution is to add a function that has access to the messenger of the components, which passes the messages to components using the messenger.


//test-module.inl
#pragma once

namespace Test
{
	template<typename MT> void pass_message(const MT& message);
}

This is included in each component's file, sample component:


//test-module-a.inl
#pragma once

#include "messenger-module.inl"
#include "test-module.inl"
#include <string>

namespace Test
{
	class module_a
	{
	public:

		std::tuple<> process_message(const int& message)
		{
			pass_message(std::to_string(message));
			return std::tuple<>();
		}

		PROCESS_KNOWN_MESSAGES_ONLY
	};
}

The declaration of the pass_message is provided in the header which allows the use in the component. The definition however is yet to come, which will follow after the instantiation of the messenger, which is as follows.


//test-messenger.inl
#pragma once

#include "messenger.inl"
#include "test-module-a.inl"
#include "test-module-b.inl"

namespace Test
{
	Messenger::messenger<Test::module_a, Test::module_b> test_messenger;
	template <typename MT> void pass_message(const MT &message)
	{
		test_messenger.pass_message(message);
	}
}

This is not an elegant way of doing it, but does the job.

Pros and cons

Cons

  • The performance relies on the optimizer to do it's job.

Pros

  • Zero overhead.
  • Increase of encapsulation and decupling.
  • Easy to implement.

Thanks for reading, looking forward to your opinions and suggestions.

I'm also looking forward on writing a graphics engine using this design pattern, hopefully I'll make time and post the progress here.

Code and VS project on GitHub: https://github.com/IYP-Programer-Yeah/MDP/

0 likes 1 comments

Comments

215648

Good job on not writing ugly code.

May 11, 2018 06:30 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement

Latest Entries

IYP Bot 2.0

2207 views

WoA V Day 7

1955 views

WoA V Day 6

2550 views

WoA V Day V

2355 views

WoA V Day 4

2217 views

WoA V Day 3

2000 views

WoA V Day 2

2213 views

WoA V Day 1

3389 views
Advertisement