Automating Basic Operations
Let's start with automation of read and write. In most cases the read()
operation of the message has to read all the fields the message contains, as well as write()
operation has to write all the fields of the message.
In order to make the generation of appropriate read/write code to be a job of the compiler we have to:
Provide the same interface for every message field.
Introduce a meta-programming friendly structure to hold all the fields, such
as
std::tuple
.Use meta-programming techniques to iterate over every field in the
bundle and invoke the required read/write function of
every field.
Let's assume, all the message fields provide the following interface:
The custom message class needs to define its fields bundled in std::tuple
What remains is to implement automatic invocation of read()
and write()
member function for every field in AllFields
tuple.
Let's take a look at standard algorithm std::for_each. Its last parameter is a functor object, which must define appropriate operator()
member function. This function is invoked for every element being iterated over. What we need is something similar, but instead of receiving iterators, it must receive a full tuple object, and the operator()
of provided functor must be able to receive any type, i.e. be a template function.
As the result the signature of such function may look like this:
where tuple
is l- or r-value reference to any std::tuple
object, and func
is l- or r-value reference to a functor object that must define the following public interface:
Implementation of thetupleForEach()
function described above can be a nice exercise for practising some meta-programming skills. Appendix A contains the required code if help is required.
Implementing Read
In order to implement read functionality there is a need to define proper reading functor class, which may receive any field:
Then the body of readImpl()
member function of the actual message class may look like this:
From now on, any modification to the AllFields
bundle of fields does NOT require any additional modifications to the body of readImpl()
function. It becomes a responsibility of the compiler to invoke read()
member function of all the fields.
Implementing Write
Implementation of the write functionality is very similar. Below is the implementation of the writer functor class:
Then the body of writeImpl()
member function of the actual message class may look like this:
Just like with reading, any modification to the AllFields
bundle of fields does NOT require any additional modifications to the body of writeImpl()
function. It becomes a responsibility of the compiler to invoke write()
member function of all the fields.
Eliminating Boilerplate Code
It is easy to notice that the body of readImpl()
and writeImpl()
of every ActualMessage*
class looks the same. What differs is the tuple of fields which get iterated over.
It is possible to eliminate such duplication of boilerplate code by introducing additional class in the class hierarchy, which receives a bundle of fields as a template parameter and implements the required functions. The same technique was used to eliminate boilerplate code for message dispatching.
All the ActualMessage*
classes need to inherit from MessageBase
while providing their own fields. The right implementation of readImpl()
and writeImpl()
is going to be generated by the compiler automatically for every custom message.
Other Basic Operations
In addition to read and write, there are other operations that can be automated. For example, the serialisation length of the full message is a summary of the serialisation lengths of all the fields. If every field can report its serialisation length, then the implementation may look like this:
NOTE, that example above used tupleAccumulate()
function, which is similar to std::accumulate. The main difference is that binary operation function object, provided to the function, must be able to receive any type, just like with tupleForEach()
described earlier. The code of tupleAccumulate()
function can be found in Appendix B.
Another example is an automation of validity check. In most cases the message is considered to be valid if all the fields are valid. Let's assume that every fields can also provide an information about validity of its data:
The implementation of message contents validity check may look like this:
Overriding Automated Default Behaviour
It is not uncommon to have some optional fields in the message, the existence of which depends on some bits in previous fields. In this case the default read and/or write behaviour generated by the compiler needs to be modified. Thanks to the inheritance relationship between the classes, nothing prevents us from overriding the readImpl()
and/or writeImpl()
function and providing the right behaviour:
The MessageBase<...>
class already contains the definition of FieldReader
and FieldWriter
helper classes, it can provide helper functions to read/write only several fields from the whole bundle. These functions can be reused in the overriding implementations of readImpl()
and/or writeImpl()
:
The provided readFieldsFromUntil()
and writeFieldsFromUntil()
protected member functions use tupleForEachFromUntil()
function to perform read/write operations on a group of selected fields. It is similar to tupleForEach()
used earlier, but receives additional template parameters, that specify indices of the fields for which the provided functor object needs to be invoked. The code of tupleForEachFromUntil()
function can be found in Appendix C.
Last updated