C++ Double Dispatch, Factory Pattern or a way to automate creating derived objects from received serial data

StackOverflow https://stackoverflow.com/questions/21506633

  •  05-10-2022
  •  | 
  •  

I'm working on a C++ communication library in which I receive serialized data from a number of devices (network sockets, uart/usb, CAN, and LIN networks). I also need to create serialized data from my message objects.

I have a base class called MessageBase from which I presently have two derived classes called Message and CtrlMessage. The project will eventually need a few more message types in the future and so I'm looking to implement using a design pattern that allows easy to expand to new message types in the future.

My other goal is, as Scott Meyes puts it, hard to use the classes incorrectly and easy to use correctly.

I began looking at using NVI pattern and using C++ factory to create messages however, the Factory class would then need to handle some of the de-serialization of a header in order to figure out what Type of message is in the payload.

class MessageBase
{
private:
  // other public & private methods omitted for brevity
  MessageBase &ISerialize( dsStream<byte> &sdata) = 0;
public:
  MessageBase &Serialize( dsStream<byte> &sdata)
  {
     ISerialize(sdata);
  }

}

class Message : public MessageBase
{
private:
    // other public & private methods omitted for brevity
   MessageBase &ISerialize( dsStream<byte> &sdata);
public:
}

class MessageFactory
{
private:

public:

   CreateMessageFromStream( dsStream<byte> &RxData)
   {
      // read N bytes from RxData to determine type and then
      // jump into switch to build message

      switch(MsgType)
      {
         case MSG_DATA:
         {
           Message *pMsg = new Message(RxData);
         }
         break;

         case MSG_CTRL:
         {
           MessageCtrl *pMsg = new MessageCtrl(RxData);
         }
         break;
      }
   }

// I shorten quite a bit of this to, hopefully, give the basic idea.

The other approach I've been studying is the Double Dispatch as outlined by Scott Meyers Item#33 in his More Effective C++ book. But that only appears to shift the problem to either requiring all the sibling derived classes to know about each other or the more advanced solution with stl map to emulate vtable. That code looks awful and hard to follow.

I took a look at the C++ visitor pattern and the Builder Creational pattern and these all require the caller to know what kind of derived Message type you want to instantiate ahead of time.

I know I could just put a big switch statement in the MessageFactory, like shown, and be done with it but what I'm after is a way to add new message types derived from MessageBase and not have to touch the MessageFactory class. I don't want other programmers to have to know or go find all the places where code needs to be updated for a new message type.

Also, this is an embedded application and that being the case certain things are off the table. I can use Template programming techniques but I do not have any STL library nor do I have any Boost library support.

Any suggestions?

有帮助吗?

解决方案

I do not know if this is worth it. But machinery to do some of what you want can be written.

We start with MessageBase. It has a private constructor.

You then tell it to make MessageHelper<T> to be a friend class.

MessageHelper looks like this:

enum MessageType {
  TYPE1, // notice no assigment
  TYPE2, // values should be consecutive, distinct, and start at `0`
  TYPE3, // or things go poorly later on.
  NUM_TYPES /* should be last */
};
template<MessageType> struct MessageTag {}; // empty, for overloading
template<MessageType...> struct MessageTags {};
template<MessageType Last, MessageType... List> struct MakeMessageTags:
  MakeMessageTags<MessageType(Last-1), MessageType(Last-1), List...>
{};
template<MessageType... List> struct MakeMessageTags<MessageType(0), List...>:
  MessageTags<List...>
{};
typedef MessageBase*(*MessageCreatorFunc)(dsStream<byte>&);

// write this somewhere, next to a given type.  If you don't, code later will fail to compile
// (yay).  You could make a macro to write these:
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) { 
  return []( dsStream<byte>& st )->MessageBase* {
    return new MessageType1(st);
  };
}

// manual compile time switch:    
template<MessageType... List>
MessageBase* CreateMessageFromStream_helper( MessageType idx, dsStream<byte>& st, MessageTags<List...> )
{
  static MessageCreatorFunc creator[] = { MessageCreator(MessageTag<List>())... };
  return creator[idx]( st );
}
MessageBase* CreateMessageFromStream( dsStream<byte>& st ) {
  // stuff, extract MessageType type
  MessageBase* msg = CreateMessageFromStream_helper( type, st, MakeMessageTags<MessageType::NUM_TYPES>() );
  // continue
}

the effect of the above code is that we automatically build a manual jump table to create our messages.

If nobody writes the MessageCreator( MessageTag<TYPE> ) overload, or it is not visible in the context of the _helper, the above fails to compile. So this ensures that if you add a new message type, you write the creation code or you break the build. Much better than a switch statement hiding somewhere.

At some spot there must be an association between MessageType and the C++ type that should be created: the above machinery just makes sure if that association is not set up, we get a compiler error.

You can have a bit more fun and get a better message by, instead of overloading on MessageCreator, you specialize:

template<MessageType TYPE>
void MessageCreator( MessageTag<TYPE> ) {
  static_assert( "You have failed to create a MessageCreator for a type" );
}
// specialization:
template<>
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) {
  return []( dsStream<byte>& st )->MessageBase* {
    return new MessageType1(st);
  };
}

which is a bit more obtuse, but might generate a better error message. (while template<> is not required for all cases, as the override also replaces the template, by the standard at least one such specialization which can compile must exist, or the program is ill formed, no diagnosis required (!?)).

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top