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 PayloadType
constructor.
Okay, then what about the message queue itself? The Queue
class’s 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:
Calling 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 Queue
’s put()
and 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.