Passing Arbitrary Messages with C++
When I was working on a isometric game engine/map editor a few years back, I thought that it would be a nice idea to use some kind of a messaging mechanism to communicate between different components of the engine. Although that implementation is far from perfect, I will try to explain my intentions behind it without digging into the higher level design patterns used in that project. Also keep in mind that I am no C++ expert and take this post with a grain of salt. Note that I’m implementing this as header only for simplicity. Please write in the comments anything that you find that can be improved. The objective is to be able to divert flow to another point in the program at anytime with a list of arbitrary parameters. Consider this pseudo-code;
void callMe(<parameters>) {
int a = parameters[0];
string b = parameters[1];
}
int main() {
// Divert execution with two parameters
callMe(14, "Hello world!");
}
One thing that comes to mind may be using boost::variant
but for a simple task like this including a 300Mb+ library was not feasible (just use it, this was just an naive and completely run-time implementation). C++17 now have std::variant
but let’s continue our discussion anyways using C++14 features only. First we need a base class to extend our type agnostic objects upon;
class MessageBase {
public:
MessageBase() {};
virtual ~MessageBase() {};
};
And a classic array to keep a bunch of these objects. This is not a great idea though as we will see later on. We will leave memory management to C++ (except for the array itself) using unique_ptr
s.
typedef std::unique_ptr<MessageData> MessageList[];
Then we need to somehow incorporate type variety into the base class. This is where we use generics;
template<typename T>
class Message : public MessageBase {
T val;
public:
Message(T i)
{
val = i;
};
~Message()
{
};
};
We now have a wrapper for val
s of various types to be determined at compile time. Now what we need is a tool to ease generating these messages;
template<typename T>
std::unique_ptr<Message<T>> MakeMessage(T val)
{
return std::make_unique<Message<T>>(val);
};
And another one to retrieve them at the callee. This function gets a reference to the particular message and casts it to the given templated type. If it the cast is not successful, it will throw at runtime. Thus we have a mechanism to check the type at runtime but keep in mind that we don’t have boundary checking yet. This also requires that we add GetMessage
as a friend function to the Message
class.
template<typename T>
class Message : public MessageBase {
T val;
template<typename U>
friend U GetMessage(std::unique_ptr<MessageData>& in);
//...
};
template<typename T>
T GetMessage(std::unique_ptr<MessageBase>& in) {
Message<T>* tmp = dynamic_cast<Message<T>*>(in.get());
if (tmp) {
return tmp->val;
}
throw "Incorrect type!";
};
Now we can see all these in action. We can inline initialize our array and pass the reference to the target function. Target function will then index the array and retrieve the unique_ptr
s and extract our parameters. As we already give GetMessage the template type, we can just use auto
keyword. Remember our pseudo-code;
void callMe(MessageList parameters) {
auto a = GetMessage<int> parameters[0];
auto b = GetMessage<std::string> parameters[1];
}
int main() {
callMe(MessageList{
MakeMessage(14),
MakeMessage(std::string("Hello world!"))
});
}
This is where things get a little complicated. Some compilers (gcc) does not like using a temporary variable MessageList
here and will not compile with taking address of temporary array
as the temporary variable gets destructed immediately. Visual Studio 2015’s and OSX’s compilers does not bother as the address is indeed being used by the callee, which is a better implementation IMO (Update: it might not be see the discussion here). But to resolve this issue and add boundary checking as a bonus, we need to change things to incorporate std::initializer_list
. Then we need to add an additional index parameter to our GetMessage function. After wrapping our code with an appropriate namespace, it becomes;
#ifndef MESSAGE_DATA_HPP
#define MESSAGE_DATA_HPP
#include <memory>
#include <initializer_list>
namespace NSMessage {
class MessageBase {
public:
MessageBase() {};
virtual ~MessageBase() {};
};
typedef std::initializer_list<std::unique_ptr<MessageBase>> MessageList;
template<typename T>
class Message : public MessageBase {
T val;
template<typename U>
friend U GetMessage(MessageList& in, int index);
public:
Message(T i)
{
val = i;
};
~Message()
{
};
};
template<typename T>
T GetMessage(MessageList& in, int index) {
if (index > in.size() - 1) {
throw "Out of bounds!";
}
const std::unique_ptr<MessageBase>& item = *(in.begin() + index);
Message<T>* tmp = dynamic_cast<Message<T>*>(item.get());
if (tmp) {
return tmp->val;
}
throw "Incorrect type!";
};
template<typename T>
std::unique_ptr<Message<T>> MakeMessage(T val)
{
return std::make_unique<Message<T>>(val);
};
}
#endif //MESSAGE_DATA_HPP
Then the usage becomes a little different too;
#include <iostream>
#include "Message.hpp"
using namespace NSMessage;
void callMe(MessageList data) {
auto a = GetMessage<std::string>(data, 0);
auto b = GetMessage<int>(data, 1);
auto c = GetMessage<float>(data, 2);
std::cout << a << b << c;
std::cin >> a;
}
int main()
{
callMe(MessageList{
MakeMessage(std::string("Hello world")),
MakeMessage(14),
MakeMessage(129.3f),
});
return 0;
}
I hope you have enjoyed it! If you did or you have thing to say, don’t hesitate to leave a comment below.