6

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?
Jarx
  • 63
  • 1
  • 1
  • 3

5 Answers5

7

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.

phooji
  • 10,086
  • 2
  • 38
  • 45
  • this is way superior solution that using virtual methods, however, where do you use the EVENT variant type in your visitor? can you extend your code example to show that? – lurscher Mar 20 '11 at 18:19
  • @lurscher: I've approved your edit -- thanks for catching the inconsistent naming of `event`. Regarding your question: I will edit in moment. – phooji Mar 20 '11 at 19:12
  • @lurscher: I've edited the answer to reflect what `get_event` might look like. Please let me know if that answers your question. – phooji Mar 20 '11 at 19:19
  • +1, Boost.Variant is quite awesome. Type-safety, polymorphism and performance (equivalent to a C union) built into construct! – Matthieu M. Mar 20 '11 at 19:24
  • @Matthieu M.: I'm happy to see you are as excited as I am ;) Thanks for the edit, also. – phooji Mar 20 '11 at 19:26
  • boost::variant is cool but be sure and read http://www.boost.org/doc/libs/1_51_0/doc/html/variant/design.html#variant.design.never-empty before using it. – odinthenerd Nov 14 '12 at 19:32
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.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • +1 for mentioning visitor pattern elsewhere and reminding me to add a token post about Boost.Variant. – phooji Mar 20 '11 at 19:23
2

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.

Chadwick
  • 944
  • 12
  • 15
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.
   }
};
dalle
  • 18,057
  • 5
  • 57
  • 81
  • 3
    The Visitor pattern might be better than `dynamic_cast`. – Ben Voigt Mar 20 '11 at 17:44
  • @Ben Voigt: Yes, the Visitor pattern would be better if the different event types are known and stable as in this case. – dalle Mar 20 '11 at 17:47
  • I think it's only necessary for the event consumer to know the types of the events it consumes, not for all consumers to know about all events. But doing that might get compicated (you'd need an adapter class derived from the event type for each event/consumer pair). Still, those adapters could probably be generated with templates. – Ben Voigt Mar 20 '11 at 17:50
1

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

bakerian
  • 76
  • 1