01 Jun 2015
Recently I was working on a C++ project where I needed a simple, lightweight way to pass messages between threads. The message passing system would need to fulfill two requirements:
I decided to try writing my own message passing system utilizing C++11 move semantics.
The idea for the high level design of the message passing system is to have a basic message type that has no payload data. Other message types with different payload data types will then be derived from the basic type. The messages shouldn’t be copyable but they should be movable.
First shot at the
Msg class implementing the basic message type:
Msg only has a message ID that identifies what kind of a message it is. There’s no payload data. The
DataMsg class template implements the message type with payload data:
DataMsg owns the payload data, so it only gives const reference to it in
getPayload(). Notice also that
DataMsg uses perfect forwarding to pass its constructor arguments to
Okay, then what about the message queue itself? The
put() member function might look something like this (ignoring locks and condition variables for now):
put() takes the message in as an rvalue reference to signify that the ownership of the message is transferred to the queue. A new
Msg instance is move constructed, wrapped to
unique_ptr and pushed to the list.
But hold on, a new
Msg instance is contructed - we lose the polymorphism here, everything is “flattened” to a
Msg! We would need a way to move construct the messages in such a way that the move constructor of the correct
Msg derivant would dynamically be called.
There is a well known solution for the same problem with copy constructors called virtual constructor idiom. It’s implemented by adding a virtual
clone() member function to all the classes in the class hierarchy. Let’s extend this idiom to move constructors and add virtual
move() functions, “virtual move constructors”, to our message classes:
move() on a message object moves the data from that object into a new one. The original object is left in valid but unspecified state. Now we can rewrite
get() member functions (again ignoring thread synchronization for now):
put() moves the data out from the
Msg instance it receives as a parameter into a new instance, and pushes that new instance to the queue.
get() moves the data out from the
Msg instance at the head of the queue into a new instance and returns the new instance.
That forms the basis of the messaging system implementation. Get the full code from GitHub.