C++: How to build an events / messaging system without void pointers?

断了今生、忘了曾经 提交于 2019-12-05 00:30:29

问题


I'd like to have a dynamic messaging system in my C++ project, one where there is a fixed list of existing events, events can be triggered anywhere during runtime, and where you can subscribe callback functions to certain events.

There should be an option for arguments passed around in those events. For example, one event might not need any arguments (EVENT_EXIT), and some may need multiple ones (EVENT_PLAYER_CHAT: Player object pointer, String with message)

The first option for making this possible is allowing to pass a void pointer as argument to the event manager when triggering an event, and receiving it in the callback function.

Although: I was told that void pointers are unsafe and I shouldn't use them.

  • How can I keep (semi) dynamic argument types and counts for my events whilst not using void pointers?

回答1:


You could use a base class, optionally abstract, and use dynamic_cast. The argument will be checked at run-time. A compile-time would probably be better, though.

class EventArgs
{
public:
   virtual ~EventArgs();
};

class PlayerChatEventArgs : public EventArgs
{
public:
   PlayerChatEventArgs(Player* player, const std::string& message);
   virtual ~PlayerChatEventArgs();
   Player* GetPlayer() const;
   const std::string& GetMessage() const;
private:
   Player* player;
   std::string message;
};

class Event
{
public:
   virtual ~Event() = 0;
   virtual void Handle(const EventArgs& args) = 0;
};

class ExitEvent : public Event
{
public:
   virtual ~ExitEvent();
   virtual void Handle(const EventArgs& /*args*/)
   {
      // Perform exit stuff.
   }
};

class PlayerChatEvent : public Event
{
public:
   virtual ~PlayerChatEvent();
   virtual void Handle(const EventArgs& args)
   {
      // this will throw a bad_cast exception if cast fails.
      const PlayerChatEventArgs& playerchatargs =
         dynamic_cast<const PlayerChatEventArgs&>(args);
      // Perform player chat stuff.
   }
};



回答2:


Since others have mentioned the visitor pattern, here is a slight twist using Boost.Variant. This library is often a good choice (or at least it has been for me) when you need a set of different behaviors based on a value. Compared to a void*, it has the benefit of static type checking: if you write a visitor class the misses one of the cases, your code will not compile rather than failing at run time.

Step 1: Define message types:

struct EVENT_EXIT { }; // just a tag, really
struct EVENT_PLAYER_CHAT { Player * p; std::string msg; };

typedef boost::variant<EVENT_EXIT,
                       EVENT_PLAYER_CHAT> event;

Step 2: Define a visitor:

struct event_handler : public boost::static_visitor<void> {
  void operator()(EVENT_EXIT const& e) {
    // handle exit event here
  }

  void operator()(EVENT_PLAYER_CHAT const& e) {
    // handle chat event here
    std::cout << e.msg << std::endl;
  }
};

This defines an event handler that nicely separates out the code for each kind of event. The existence of all operator() overloads is checked at compile time (on template instantiation), so if you add an event type later, the compiler will force you to add corresponding handler code.

Note that event_handler subclasses boost::static_visitor<void>. This determines the return type for each of the operator() overloads.

Step 3: Use your event handler:

event_handler handler;
// ...
event const& e = get_event(); //variant type
boost::apply_visitor(handler, e); // will not compile unless handler
                                  // implements operator() for each
                                  // kind of event

Here, apply_visitor will call the appropriate overload for the 'actual' value of e. For example, if we define get_event as follows:

event get_event() {
  return EXIT_EVENT();
}

Then the return value will be converted implicitly to event(EXIT_EVENT()). Then apply_visitor will call the corresponding operator()(EXIT_EVENT const&) overload.




回答3:


Templates would allow you to write a type-safe event manager without it knowing the message types a-priori.

If the event types change at runtime, or you need to mix multiple types into a single container, you can use pointers to a common base class of all the message/event types.




回答4:


Something I've done in the past is set up a delegate-based system not unlike what is in C#, using the (excellent) FastDelegate library: http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible

So with that in hand, I created some general-purpose Event classes to contain the lists of delegates, like so:

template <class T1>
class Event1 {
public:
    typedef FastDelegate1<T1> Delegate;
private:
    std::vector<Delegate> m_delegates;
public:
    // ...operator() to invoke, operators += and -= to add/remove subscriptions
};

// ...more explicit specializations for diff arg counts (Event2, etc.), unfortunately

Then you can have the various sub-components expose their specific event objects (I used an interface-style but that's not necessary):

typedef Event2<Player*, std::string> PlayerChatEvent;

class IPlayerEvents {
public:
    virtual PlayerChatEvent& OnPlayerChat() = 0;
    virtual PlayerLogoutEvent& OnPlayerLogout() = 0; // etc...
};

Consumers of this interface can register like so:

void OtherClass::Subscribe(IPlayerEvent& evts) {
    evts.OnPlayerChat() += MakeDelegate(this, &OtherClass::OnPlayerChat);
}

void OtherClass::OnPlayerChat(Player* player, std::string message) {
    // handle it...
}

The result is is all individually static-typed per event type--no dynamic_casting. However it does decentralize the event system, which may or may not be an issue for your architecture.




回答5:


I would think about having a base class for the messages, and then derive all the messages from that base class. You will then be passing pointers to the base class around the events.

You will, presumably, have some basic funcitonaliy in the base class, which may include a member saying what type of message it is. This will allow you to check the message type before casting to the version you need.

It is tempting to have the base class as the most basic type of message, but I would advise making it a virtual class so that every message has to be cast to be used. This symmetry makes it much less prone to bugs later when the complexity (inevitably) increases



来源:https://stackoverflow.com/questions/5370171/c-how-to-build-an-events-messaging-system-without-void-pointers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!