我希望在我的C ++项目中有一个动态消息传递系统,其中有一个固定的现有事件列表,事件可以在运行时的任何地方触发,以及在哪里可以为某些事件订阅回调函数。
在这些事件中传递的参数应该有一个选项。例如,一个事件可能不需要任何参数(EVENT_EXIT)
,有些可能需要多个参数(EVENT_PLAYER_CHAT: Player object pointer, String with message)
使这成为可能的第一个选择是允许在触发事件时将void指针作为参数传递给事件管理器,并在回调函数中接收它。
虽然:我被告知无效指针是不安全的,我不应该使用它们。
答案 0 :(得分:7)
由于其他人提到了访问者模式,因此使用Boost.Variant进行了轻微的改动。当您需要基于值的一组不同行为时,此库通常是一个不错的选择(或者至少对我而言)。与void*
相比,它具有静态类型检查的好处:如果您编写的访问者类错过了其中一种情况,那么您的代码将无法编译而不是在运行时失败。
第1步:定义消息类型:
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;
第2步:定义访问者:
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;
}
};
这定义了一个事件处理程序,可以很好地分离出每种事件的代码。在编译时(在模板实例化时)检查是否存在所有operator()
重载,所以如果稍后添加事件类型,编译器将强制您添加相应的处理程序代码。
请注意event_handler
子类boost::static_visitor<void>
。这决定了每个operator()
重载的返回类型。
第3步:使用您的事件处理程序:
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
此处,apply_visitor
会为e
的“实际”值调用适当的重载。例如,如果我们按如下方式定义get_event
:
event get_event() {
return EXIT_EVENT();
}
然后返回值将隐式转换为event(EXIT_EVENT())
。然后apply_visitor
将调用相应的operator()(EXIT_EVENT const&)
重载。
答案 1 :(得分:3)
模板允许您编写类型安全的事件管理器,而无需事先了解消息类型。
如果事件类型在运行时更改,或者您需要将多个类型混合到一个容器中,则可以使用指向所有消息/事件类型的公共基类的指针。
答案 2 :(得分:2)
我过去做过的事情是建立一个基于委托的系统,与使用(优秀)FastDelegate库时的C#不同:http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible
所以有了这个,我创建了一些通用的Event类来包含委托列表,如下所示:
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
然后你可以让各种子组件公开他们的特定事件对象(我使用了接口风格,但这不是必需的):
typedef Event2<Player*, std::string> PlayerChatEvent;
class IPlayerEvents {
public:
virtual PlayerChatEvent& OnPlayerChat() = 0;
virtual PlayerLogoutEvent& OnPlayerLogout() = 0; // etc...
};
此界面的消费者可以这样注册:
void OtherClass::Subscribe(IPlayerEvent& evts) {
evts.OnPlayerChat() += MakeDelegate(this, &OtherClass::OnPlayerChat);
}
void OtherClass::OnPlayerChat(Player* player, std::string message) {
// handle it...
}
结果是每个事件类型都是单独的静态类型 - 没有dynamic_casting。然而,它确实分散了事件系统,这可能是您的架构的问题,也可能不是。
答案 3 :(得分:1)
您可以使用基类,可选择抽象,并使用dynamic_cast
。该参数将在运行时检查。但编译时可能会更好。
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.
}
};
答案 4 :(得分:1)
我会考虑为消息创建一个基类,然后从该基类派生所有消息。然后,您将在事件周围传递指向基类的指针。
你可能会在基类中有一些基本的功能,可能包括一个成员说它是什么类型的消息。这将允许您在转换为所需的版本之前检查消息类型。
将基类作为最基本的消息类型很诱人,但我建议将其作为一个虚拟类,以便每个消息都必须被强制转换才能使用。当复杂性(不可避免地)增加时,这种对称性使得它更不容易出现错误