There is a need to have a generic way to handle such cases.
Implementing Read
Let's start with implementing the read first.
namespace comms
{
// TField is type of the field used to read/write checksum value
// TCalc is generic class that is responsible to implement checksum calculation logic
// TNext is the next layer this one wraps
template <typename TField, typename TCalc, typename TNext>
class ChecksumLayer
{
public:
// Type of the field object used to read/write SYNC prefix.
using Field = TField;
// Take type of the ReadIterator from the next layer
using ReadIterator = typename TNext::ReadIterator;
// Take type of the WriteIterator from the next layer
using WriteIterator = typename TNext::WriteIterator;
// Take type of the message interface from the next layer
using Message = typename TNext::Message;
// Take type of the message interface pointer from the next layer
using MsgPtr = typename TNext::MsgPtr;
template <typename TMsgPtr>
ErrorStatus read(TMsgPtr& msgPtr, ReadIterator& iter, std::size_t len)
{
Field field;
if (len < field.length()) {
return ErrorStatus::NotEnoughData;
}
auto fromIter = iter; // The read is expected to use random-access iterator
auto es = m_next.read(iter, len - field.length());
if (es != ErrorStatus::Success) {
return es;
}
auto consumedLen = static_cast<std::size_t>(std::distance(fromIter, iter));
auto remLen = len - consumedLen;
es = field.read(iter, remLen);
if (es != ErrorStatus::Success) {
msgPtr.reset();
return es;
}
auto checksum = TCalc()(fromIter, consumedLen);
auto expectedValue = field.value();
if (expectedValue != checksum) {
msgPtr.reset(); // Delete allocated message
return ErrorStatus::ProtocolError;
}
return ErrorStatus::Success;
}
private:
TNext m_next;
};
} // namespace comms
It is clear that TCalc class is expected to have operator() member function, which receives the iterator for reading and number of bytes in the buffer.
As an example let's implement generic total sum of bytes calculator:
Now, let's tackle the write problem. As it was mentioned earlier, there is a need to recognise the type of the iterator used for writing and behave accordingly. If the iterator is properly defined, the std::iterator_traits class will define iterator_category internal type.
using WriteIteratorCategoryTag =
typename std::iterator_traits<WriteIterator>::iterator_category;
For random access iterators the WriteIteratorCategoryTag class will be either std::random_access_iterator_tag or any other class that inherits from it. Using this information, we can use Tag Dispatch Idiom to choose the right writing functionality.
Please pay attention, that writeOutput() function above is unable to properly calculate the checksum of the written data. There is no way the iterator can be reversed back and used as input instead of output. As the result the function returns special error status: ErrorStatus::UpdateRequired. It is an indication that the write operation is not complete and the output should be updated using random access iterator.
Implementing Update
namespace comms
{
template <...>
class ChecksumLayer
{
public:
template <typename TIter>
ErrorStatus update(TIter& iter, std::size_t len) const
{
Field field;
auto fromIter = iter;
auto es = m_next.update(iter, len - field.length());
if (es != ErrorStatus::Success) {
return es;
}
auto consumedLen = static_cast<std::size_t>(std::distance(fromIter, iter));
auto remLen = len - consumedLen;
field.value() = TCalc()(fromIter, consumedLen);
es = field.write(iter, remLen);
return es;
}
private:
TNext m_next;
};
} // namespace comms
Please note, that every other layer must also implement the update() member function, which will just advance the provided iterator by the number of bytes required to write its field and invoke update() member function of the next (wrapped) layer.
And so on for the rest of the layers. Also note, that the code above will work, only when the field has the same serialisation length for any value. If this is not the case (Base-128 encoding is used), the previously written value needs to be read, instead of just advancing the iterator, to make sure the iterator is advanced right amount of bytes:
template <typename TIter>
ErrorStatus update(TIter& iter, std::size_t len) const
{
TField field;
auto es = field.read(iter);
if (es != ErrorStatus::Success) {
return es;
}
return m_next.update(iter, len - field.length());
}
The variable serialisation length encoding will be forced using some kind of special option. It can be identified at compile time and Tag Dispatch Idiom can be used to select appropriate update functionality.
The caller, that requests protocol stack to serialise a message, must check the error status value returned by the write() operation. If it is ErrorStatus::UpdateRequired, the caller must create random-access iterator to the already written buffer and invoke update() function with it, to make sure the written information is correct.
using ProtocolStack = ...;
ProtocolStack protStack;
ErrorStatus sendMessage(const Message& msg)
{
ProtocolStack::WriteIterator writeIter = ...;
auto es = protStack.write(msg, writeIter, bufLen);
if (es == ErrorStatus::UpdateRequired) {
auto updateIter = ...; // Random access iterator to written data
es = protStack.update(updateIter, ...);
}
return es;
}