C ++: Как создать систему событий / сообщений без пустых указателей? - PullRequest
6 голосов
/ 20 марта 2011

Я хотел бы иметь динамическую систему обмена сообщениями в моем проекте C ++, где есть фиксированный список существующих событий, события могут запускаться где угодно во время выполнения и где вы можете подписать функции обратного вызова на определенные события. *

Должна быть опция для аргументов, передаваемых в этих событиях. Например, для одного события могут не потребоваться аргументы (EVENT_EXIT), а для некоторых может потребоваться несколько (EVENT_PLAYER_CHAT: Player object pointer, String with message)

Первый вариант, позволяющий сделать это возможным, - разрешить передачу указателя void в качестве аргумента менеджеру событий при запуске события и получении его в функции обратного вызова.

Хотя: мне сказали, что указатели пустоты небезопасны, и я не должен их использовать.

  • Как сохранить (полу) динамические типы аргументов и рассчитывать их события, не используя указатели void?

Ответы [ 5 ]

7 голосов
/ 20 марта 2011

Поскольку другие упомянули шаблон посетителей, здесь приведен небольшой поворот с использованием 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&) перегрузку.

3 голосов
/ 20 марта 2011

Шаблоны позволят вам написать менеджер событий, безопасный для типов, не зная априорных типов сообщений.

Если типы событий изменяются во время выполнения или вам нужно смешать несколько типов в одном контейнере, вы можете использовать указатели на общий базовый класс всех типов сообщений / событий.

2 голосов
/ 20 марта 2012

Что-то, что я делал в прошлом, - это установка системы на основе делегатов, мало чем отличающейся от того, что есть в C #, с использованием (превосходной) библиотеки FastDelegate: 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...
}

В результате все статически типизируются для каждого типа события - динамическая трансляция отсутствует.Однако это децентрализует систему событий, что может или не может быть проблемой для вашей архитектуры.

1 голос
/ 20 марта 2011

Я бы подумал о наличии базового класса для сообщений, а затем получил бы все сообщения из этого базового класса.Затем вы будете передавать указатели на базовый класс вокруг событий.

У вас, по-видимому, будут базовые функции в базовом классе, которые могут включать члена, говорящего о том, какой это тип сообщения.Это позволит вам проверить тип сообщения перед приведением к нужной вам версии.

Заманчиво иметь базовый класс в качестве наиболее базового типа сообщения, но я бы посоветовал сделать его виртуальным классом, чтобыкаждое сообщение должно быть передано для использования.Эта симметрия делает ее гораздо менее подверженной ошибкам позже, когда сложность (неизбежно) увеличивается

1 голос
/ 20 марта 2011

Вы можете использовать базовый класс, при желании абстрактный, и использовать 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.
   }
};
...