Те системы событий, с которыми я сталкивался, страдали от таких проблем:
Перегруженность интерфейса — макросы, громоздкие шаблоны, неочевидный синтаксис, множественная параметризация;
Broadcast — каждое событие отправляется всем слушателям, а они сами решают, нужно ли им реагировать. Это просто, но дорого;
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
Файл 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, чтобы определить "горячая фаза" или нет. Если фаза не горячая, то и добавлять/удалять можно напрямую.
Некоторые тонкости горячей фазы
Добавление/Удаление хендлеров во время диспатчинга:
Добавленый хендлер - не будет вызван в текущем диспатчинге.
Удаленный хендлер - будет помечен как невалидный и не будет вызван в текущем диспатчинге.
Удаление мгновенное, а добавление отложенное.
Проблемы и ограничения
Можно забыть отписаться. Если объект будет уничтожен и не отпишется от события, возникнет UB из-за висячего указателя.
Лямбды запрещены, поскольку их невозможно отписать.
Не thread-safe.
Для чего эта система не подойдет
«Я хочу слушать событие, но я не знаю кто его отправляет».
«Я не знаю(или не хочу знать) время жизни объекта с событием».
Для чего эта система подойдет
Вы имеете доступ к источнику события.
Вы точно знаете время жизни источника события(или можете убедиться, что источник жив)
Возможные улучшения
ScopedEventHandler — RAII‑механизм, отписка по разрушению. Решит проблему забывчивости, а так же даст возможность подписывать лямбды и функторы.
Отказ от виртуальных вызовов может дать прирост за счёт устранения обращения к
vtable
.
Заключение
Решение уровня «добавил и поехало». Требует внимательности, поскольку, забыв отписаться, можно получить UB. Подходит для однопоточной архитектуры, где явно известны источники событий и их жизненный цикл.
Если будет интересно в следующем посте напишу:
ScopedEventHandler
Реализацию без virtual
Комментарии и конструктивная критика приветствуется. Будет интересно мнение экспертов.