English 中文(简体)
Choosing the right subclass to instantiate programmatically
原标题:

Ok, the context is some serialization / deserialization code that will parse a byte stream into an object representation that s easier to work with (and vice-versa).

Here s a simplified example with a base message class and then depending on a type header, some more data/function are present and we must choose the right subclass to instantiate:

class BaseMessage {
public:
    enum Type {
        MyMessageA = 0x5a,
        MyMessageB = 0xa5,
    };

    BaseMessage(Type type) : mType(type) { }
    virtual ~BaseMessage() { }

    Type type() const { return mType; } 

protected:
    Type mType;

    virtual void parse(void *data, size_t len);
};

class MyMessageA {
public:
    MyMessageA() : BaseMessage(MyMessageA) { }

    /* message A specific stuf ... */

protected:
    virtual void parse(void *data, size_t len);
};

class MyMessageB {
public:
    MyMessageB() : BaseMessage(MyMessageB) { }

    /* message B specific stuf ... */

protected:
    virtual void parse(void *data, size_t len);
};

In a real examples, there would be hundreds of different message types and possibly several level or hierarchy because some messages share fields/functions with each other.

Now, to parse a byte string, I m doing something like:

BaseMessage *msg = NULL;
Type type = (Type)data[0];

switch (type) {
    case MyMessageA:
        msg = new MyMessageA();
        break;

    case MyMessageB:
        msg = new MyMessageB();
        break;

    default:
        /* protocol error */
}

if (msg)
    msg->parse(data, len);

But I don t find this huge switch very elegant, and I have the information about which message has which type value twice (once in the constructor, one in this switch) It s also quite long ...

I m looking for a better way that would just be better ... How to improve this?

最佳回答

It s a pretty basic question in fact (as you can imagine, you are definitely not the only one deserializing in C++).

What you are looking for is called Virtual Construction.

C++ does not define Virtual Construction, but it s easy to approximate it using the Prototype Design Pattern or using a Factory method.

I personnally prefer the Factory approach, for the reason that the Prototype one means having some kind of default instance that is replicated and THEN defined... the problem is that not all classes have a meaningful default, and for that matter, a meaningful Default Constructor.

The Factory approach is easy enough.

  • You need a common base class for the Messages, and another for the Parsers
  • Each Message has both a Tag and an associated Parser

Let s see some code:

// Framework
class Message
{
public:
  virtual ~Message();
};

class Parser
{
public:
  virtual ~Parser();
  virtual std::auto_ptr<Message> parse(std::istream& serialized) const;
};

// Factory of Messages
class MessageFactory
{
public:
  void register(std::string const& tag, Parser const& parser);
  std::auto_ptr<Message> build(std::string const& tag, std::istream& serialized) const;
private:
  std::map<std::string,Parser const*> m_parsers;
};

And with this framework (admittedly simple), some derived classes:

class MessageA: public Message
{
public:
  MessageA(int a, int b);
};

class ParserA: public Parser
{
public:
  typedef std::auto_ptr<MessageA> result_type;
  virtual result_type parse(std::istream& serialized) const
  {
    int a = 0, b = 0;
    char space = 0;
    std::istream >> a >> space >> b;
    // Need some error control there
    return result_type(new MessageA(a,b));
  }
};

And at last, the use:

int main(int argc, char* argv[])
{
  // Register the parsers
  MessageFactory factory;
  factory.register("A", ParserA());

  // take a file
  // which contains  A 1 2
 
  std::ifstream file = std::ifstream("file.txt");
  std::string tag;
  file >> tag;
  std::auto_ptr<Message> message = factory.parse(tag, file);

  // message now points to an instance of MessageA built by MessageA(1,2)
}

It works, I know for I use it (or a variation).

There are some things to consider:

  • You may be willing to make MessageFactory a singleton, this then allows it to be called at library load, and thus you can register your parsers by instantiating static variables. This is very handy if you don t want main to have to register every single parser type: locality > less dependencies.
  • The tags have to be shared. It is not unusual either for the tag to be served by a virtual method of the Message class (called tag).

Like:

class Message
{
public:
  virtual ~Message();
  virtual const std::string& tag() const = 0;
  virtual void serialize(std::ostream& out) const;
};
  • The logic for serialization has to be shared too, it is not unusual for an object to handle its own serialization/deserialization

Like:

class MessageA: public Message
{
public:
  static const std::string& Tag();
  virtual const std::string& tag() const;
  virtual void serialize(std::ostream& out) const;

  MessageA(std::istream& in);
};

template <class M>
class ParserTemplate: public Parser // not really a parser now...
{
public:
  virtual std::auto_ptr<M> parse(std::istream& in) const
  {
    return std::auto_ptr<M>(new M(in));
  }
};

What s great with templates is that it never stops to amaze me

class MessageFactory
{
public:
  template <class M>
  void register()
  {
    m_parsers[M::Tag()] = new ParserTemplate<M>();
  }
};

//skipping to registration
  factory.register<MessageA>();

Now isn t it pretty :) ?

问题回答

One way of approaching it would be using a map and register some kind of factory function for each message type. This means that you get rid of the switch case and can add and remove messages dynamically.

The code would look something like:

// Create the map (most likely a member in a different class)
std::map<BaseMessage::Type, MessageCreator*> messageMap;
...

// Register some message types
// Note that you can add and remove messages at runtime here
messageMap[BaseMessage::MyMessageA] = new MessageCreatorT<BaseMessageA>();
messageMap[BaseMessage::MyMessageB] = new MessageCreatorT<BaseMessageB>();
...

// Handle a message
std::map<Type, MessageCreator*>::const_iterator it = messageMap.find(msgType);
if(it == messageMap.end()) {
    // Unknown message type
    beepHang();
}
// Now create the message
BaseMessage* msg = it->second.createMessage(data);

The MessageCreator class would look something like this:

class MessageCreator {
    public:
    virtual BaseMessage* createMessage(void* data, size_t len) const = 0;
};
template<class T> class MessageCreatorT : public MessageCreator {
    public:
    BaseMessage* createMessage(void* data, size_t len) const {
        T* newMessage = new T();
        newMessage.parse(data, len);
        return newMessage;
    }
};




相关问题
Undefined reference

I m getting this linker error. I know a way around it, but it s bugging me because another part of the project s linking fine and it s designed almost identically. First, I have namespace LCD. Then I ...

C++ Equivalent of Tidy

Is there an equivalent to tidy for HTML code for C++? I have searched on the internet, but I find nothing but C++ wrappers for tidy, etc... I think the keyword tidy is what has me hung up. I am ...

Template Classes in C++ ... a required skill set?

I m new to C++ and am wondering how much time I should invest in learning how to implement template classes. Are they widely used in industry, or is this something I should move through quickly?

Print possible strings created from a Number

Given a 10 digit Telephone Number, we have to print all possible strings created from that. The mapping of the numbers is the one as exactly on a phone s keypad. i.e. for 1,0-> No Letter for 2->...

typedef ing STL wstring

Why is it when i do the following i get errors when relating to with wchar_t? namespace Foo { typedef std::wstring String; } Now i declare all my strings as Foo::String through out the program, ...

C# Marshal / Pinvoke CBitmap?

I cannot figure out how to marshal a C++ CBitmap to a C# Bitmap or Image class. My import looks like this: [DllImport(@"test.dll", CharSet = CharSet.Unicode)] public static extern IntPtr ...

Window iconification status via Xlib

Is it possible to check with the means of pure X11/Xlib only whether the given window is iconified/minimized, and, if it is, how?

热门标签