C++ Event System от идеи до реализации

от автора

Те системы событий, с которыми я сталкивался, страдали от таких проблем:

  1. Перегруженность интерфейса — макросы, громоздкие шаблоны, неочевидный синтаксис, множественная параметризация;

  2. Broadcast — каждое событие отправляется всем слушателям, а они сами решают, нужно ли им реагировать. Это просто, но дорого;

  3. Signal/Slot архитектура, как в Qt — требует кодогенерации и тяжело отделяется от инфраструктуры.

Я захотел реализовать собственную систему событий, которая была бы:

  • простой в использовании;

  • понятной в коде;

  • симметричной — добавление и удаление обработчиков по одинаковому интерфейсу;

  • легкой — минимум кода;

  • самодостаточной — без макросов, фреймворков, кодогенерации или внешних зависимостей;

Пример использования

class SomeClass { public: Event<> someEvent; Event<int, SomeClass*> otherEvent; private: void dispatchSomeEvent() { someEvent(); }      void dispatchOtherMethod()     {         otherEvent(5, this);     } };  class SomeOtherClass { public:     ~SomeOtherClass()     { m_someClass->someEvent.RemoveHandler(*this, &SomeOtherClass::onSomeEvent); m_someClass->otherEvent.RemoveHandler(*this, &SomeOtherClass::onSomeOtherEvent);     } void onSomeClassCreated(SomeClass* someClass) { m_someClass = someClass; m_someClass->someEvent.AddHandler(*this, &SomeOtherClass::onSomeEvent); m_someClass->otherEvent.AddHandler(*this, &SomeOtherClass::onSomeOtherEvent); } void onSomeEvent() { //do something and unsubscribe m_someClass->someEvent.RemoveHandler(*this, &SomeOtherClass::onSomeEvent); }  void onSomeOtherEvent(int val, SomeClass* obj) { } private: SomeClass* m_someClass; }; 

Реализация

details.inl

  1. Файл details.inl

namespace detail { inline std::size_t hash_combine(std::size_t seed, std::size_t value) noexcept { return seed ^ (value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2)); }  template<typename... Args> inline std::size_t GenerateID(void (*func)(Args...)) { return std::hash<void*>()(reinterpret_cast<void*>(func)); }  template<typename Obj, typename Meth> inline std::size_t GenerateID(Obj* obj, Meth method) { constexpr std::size_t N = sizeof(Meth); constexpr std::size_t W = sizeof(std::size_t); constexpr std::size_t CHUNKS = (N + W - 1) / W;  std::size_t pieces[CHUNKS]{}; std::memcpy(pieces, &method, N);  std::size_t h = std::hash<std::type_index>{}(typeid(Obj)); for (std::size_t v : pieces) h = hash_combine(h, std::hash<std::size_t>{}(v));  h = hash_combine(h, std::hash<void*>{}(static_cast<void*>(obj))); return h; } } 

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

EventHandler

Посмотрим как реализуется базовый EventHandler:

template<typename ...Args> class EventHandler { public: virtual void call(Args&&... args) = 0;  size_t GetHandlerID() const { return m_handlerID; }  void Invalidate() { m_valid = false; } bool IsValid() const { return m_valid; }  protected: size_t m_handlerID; private: bool m_valid = true; };

Это база для разных видов хендлеров. Чистая функция вызова.
Тут же реализована инвалидация хендлера для «горячего» удаления.

MethodEventHandler

Наследник для функций-членов:

template<typename Object, typename ...Args> class MethodEventHandler final : public EventHandler<Args...> { public: using MethodType = void(Object::*)(Args...); public: MethodEventHandler(Object& object, MethodType method) : m_object(object), m_method(method) { this->m_handlerID = detail::GenerateID(&object, method); }  void call(Args&&... args) override { (m_object.*m_method)(std::forward<Args>(args)...); }  private: Object& m_object; MethodType m_method; };
  • Хранит ссылку на объект и указатель на функцию.

  • Переопределяет call, так как синтаксис вызова специфический.

  • Генерируется айди(хеш) на базе объекта и функции-члена.

FunctionEventHandler

Наследник для функции:

template<typename ...Args> class FunctionEventHandler : public EventHandler<Args...> { public: using FunctionType = void(*)(Args...); public: FunctionEventHandler(FunctionType function) : m_function(function) { this->m_handlerID = detail::GenerateID(function); }  void call(Args&&... args) override { (*m_function)(std::forward<Args>(args)...); }  private: FunctionType m_function; };

Здесь — тот же подход, только для функций.

Event

Реализация самого Event:

template<typename ...Args> class Event { using HandlerType = EventHandler<Args...>;  public: //Защитимся от копирования и переноса - это недопустимая операция(во всяком случае пока) Event() = default; Event(const Event&) = delete; Event& operator=(const Event&) = delete; Event(Event&&) = delete; Event& operator=(Event&&) = delete;  template<typename Object> void AddHandler(Object& object, MethodEventHandler<Object, Args...>::MethodType method) { if (HasId(detail::GenerateID(&object, method))) return;  (m_dispatching ? m_added_handlers : m_handlers).emplace_back(std::make_unique<MethodEventHandler<Object, Args...>>(object, method)); }  void AddHandler(FunctionEventHandler<Args...>::FunctionType function) { if (HasId(detail::GenerateID(function))) return;  (m_dispatching ? m_added_handlers : m_handlers).emplace_back(std::make_unique<FunctionEventHandler<Args...>>(function)); }  template<class F> void AddHandler(F) = delete; // Лямбды и функторы не поддерживаются — нельзя безопасно отписаться  template<typename Object> void RemoveHandler(Object& object, MethodEventHandler<Object, Args...>::MethodType method) { RemoveById(detail::GenerateID(&object, method)); }  void RemoveHandler(FunctionEventHandler<Args...>::FunctionType function) { RemoveById(detail::GenerateID(function)); }  template<class F> void RemoveHandler(F) = delete; //Симметрия  void operator()(Args... args) { m_dispatching = true; for (auto& handler : m_handlers) if (handler->IsValid()) handler->call(std::forward<Args>(args)...); m_dispatching = false;  // Удаляем невалидные обработчики после завершения вызовов         if (m_wasHotRemoved)         {     m_handlers.erase(std::remove_if(m_handlers.begin(), m_handlers.end(), [](const auto& handler) { return !handler->IsValid(); }), m_handlers.end());             m_wasHotRemoved = false;         } for (auto& handler : m_added_handlers) m_handlers.push_back(std::move(handler)); m_added_handlers.clear(); }  private: std::vector<std::unique_ptr<HandlerType>> m_handlers; std::vector<std::unique_ptr<HandlerType>> m_added_handlers;  bool m_dispatching = false;     bool m_wasHotRemoved = false;  bool HasId(std::size_t id) const { auto pred = [&](const auto& handler) { return handler->GetHandlerID() == id; }; return std::any_of(m_handlers.begin(), m_handlers.end(), pred) || std::any_of(m_added_handlers.begin(), m_added_handlers.end(), pred); }  void RemoveById(std::size_t id) { auto pred = [&](const auto& handler) { return handler->GetHandlerID() == id; };  if (m_dispatching) { if (auto it = std::find_if(m_handlers.begin(), m_handlers.end(), pred); it != m_handlers.end()) {                 (*it)->Invalidate();                 m_wasHotRemoved = true;             } } else { m_handlers.erase(std::remove_if(m_handlers.begin(), m_handlers.end(), pred), m_handlers.end()); }  m_added_handlers.erase(std::remove_if(m_added_handlers.begin(), m_added_handlers.end(), pred), m_added_handlers.end()); } };
  • Предлагается 2 функции добавления и 2 функции удаления хендлеров в зависимости от типа колбека.

  • За счет возможности инвалидировать хендлер можно удалять во время диспатчинга события.

  • Работа с ID(хешом) — скрытая реализация, по этому все убрано в private зону и унифицировано для разных типов хендлеров.

  • Специальный флажок m_dispatching, чтобы определить «горячая фаза» или нет. Если фаза не горячая, то и добавлять/удалять можно напрямую.

Некоторые тонкости горячей фазы

Добавление/Удаление хендлеров во время диспатчинга:

  • Добавленый хендлер — не будет вызван в текущем диспатчинге.

  • Удаленный хендлер — будет помечен как невалидный и не будет вызван в текущем диспатчинге.

Удаление мгновенное, а добавление отложенное.

Проблемы и ограничения

  1. Можно забыть отписаться. Если объект будет уничтожен и не отпишется от события, возникнет UB из-за висячего указателя.

  2. Лямбды запрещены, поскольку их невозможно отписать.

  3. Не thread-safe.

Для чего эта система не подойдет

  1. «Я хочу слушать событие, но я не знаю кто его отправляет».

  2. «Я не знаю(или не хочу знать) время жизни объекта с событием».

Для чего эта система подойдет

  1. Вы имеете доступ к источнику события.

  2. Вы точно знаете время жизни источника события(или можете убедиться, что источник жив)

Возможные улучшения

  1. ScopedEventHandler — RAII‑механизм, отписка по разрушению. Решит проблему забывчивости, а так же даст возможность подписывать лямбды и функторы.

  2. Отказ от виртуальных вызовов может дать прирост за счёт устранения обращения к vtable.

Заключение

Решение уровня «добавил и поехало». Требует внимательности, поскольку, забыв отписаться, можно получить UB. Подходит для однопоточной архитектуры, где явно известны источники событий и их жизненный цикл.

Если будет интересно в следующем посте напишу:

  1. ScopedEventHandler

  2. Реализацию без virtual

Комментарии и конструктивная критика приветствуется. Будет интересно мнение экспертов.

GitHub


ссылка на оригинал статьи https://habr.com/ru/articles/924138/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *