C++:如何构建没有空指针的事件/消息系统?

发布于 2024-10-25 15:11:07 字数 374 浏览 5 评论 0原文

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

应该有一个选项可以在这些事件中传递参数。例如,一个事件可能不需要任何参数 (EVENT_EXIT),而有些事件可能需要多个 (EVENT_PLAYER_CHAT: 玩家对象指针,带有消息的字符串)

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

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

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

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

爱她像谁 2024-11-01 15:11:07

由于其他人已经提到了访问者模式,这里使用 Boost 进行了轻微的改动.变体。当您需要一组基于值的不同行为时,这个库通常是一个不错的选择(或者至少对我来说是这样)。与 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_handlerboost::static_visitor 的子类。这决定了每个 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&)重载。

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.

回眸一笑 2024-11-01 15:11:07

模板允许您编写类型安全的事件管理器,而无需先验了解消息类型。

如果事件类型在运行时发生变化,或者您需要将多种类型混合到一个容器中,则可以使用指向所有消息/事件类型的公共基类的指针。

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.

↘人皮目录ツ 2024-11-01 15:11:07

我过去所做的事情是使用(优秀的)FastDelegate 库建立一个基于委托的系统,与 C# 中的系统不同:http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible

因此,有了这些,我创建了一些通用的事件类来包含委托列表,如下所示:

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...
}

结果是每个事件类型都是单独静态类型的——没有动态类型转换。然而,它确实分散了事件系统,这对于您的架构来说可能是也可能不是问题。

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.

自此以后,行同陌路 2024-11-01 15:11:07

您可以使用基类(可选的抽象类)并使用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.
   }
};

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.
   }
};
反目相谮 2024-11-01 15:11:07

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

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

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

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文