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.
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.
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.
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.
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.
}
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With