C ++:如何在没有void指针的情况下构建事件/消息传递系统?

时间:2011-03-20 17:28:18

标签: c++ events messaging

我希望在我的C ++项目中有一个动态消息传递系统,其中有一个固定的现有事件列表,事件可以在运行时的任何地方触发,以及在哪里可以为某些事件订阅回调函数。

在这些事件中传递的参数应该有一个选项。例如,一个事件可能不需要任何参数(EVENT_EXIT),有些可能需要多个参数(EVENT_PLAYER_CHAT: Player object pointer, String with message)

使这成为可能的第一个选择是允许在触发事件时将void指针作为参数传递给事件管理器,并在回调函数中接收它。

虽然:我被告知无效指针是不安全的,我不应该使用它们。

  • 如何在不使用void指针的情况下保留(半)动态参数类型和事件计数?

5 个答案:

答案 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)

我会考虑为消息创建一个基类,然后从该基类派生所有消息。然后,您将在事件周围传递指向基类的指针。

你可能会在基类中有一些基本的功能,可能包括一个成员说它是什么类型的消息。这将允许您在转换为所需的版本之前检查消息类型。

将基类作为最基本的消息类型很诱人,但我建议将其作为一个虚拟类,以便每个消息都必须被强制转换才能使用。当复杂性(不可避免地)增加时,这种对称性使得它更不容易出现错误