Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?
like image 654
Jarx Avatar asked Mar 20 '11 17:03

Jarx


4 Answers

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.

like image 122
phooji Avatar answered Sep 29 '22 23:09

phooji


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.

like image 21
Ben Voigt Avatar answered Sep 30 '22 01:09

Ben Voigt


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.

like image 44
Chadwick Avatar answered Sep 30 '22 00:09

Chadwick


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.
   }
};
like image 41
dalle Avatar answered Sep 29 '22 23:09

dalle