Простая система событий на С++11
Автор: 0r@ngE
При проектировании своих игр/программ/систем мы стараемся как можно больше следовать принципу SRP (Single responsibility principle) [1] - то есть каждая сущность инкапсулирует в себя одну конкретную обязанность.
И очень замечательно если получается избегать прямого общения сущностей между собой, однако в геймдеве этого, порой, бывает сложно добиться, и некоторым сущностям хотелось бы дать возможность мгновенного реагирования на те или иные события.
Это можно решить, например, использовав паттерн Observer [2]. Однако данный паттерн не очень удобен, в первую очередь тем, что приходится использовать множественное наследование, а так же включать заголовочные файлы класса-слушателя. Хочется решать задачу более «чистым» способом.
И тут мы приходим к идее Событий (Events) [3]. В целом, идея не нова, и в том или ином виде используется давно и много кем. Частный случай событийной системы – система сигнал-слот, которая хоть и избавляет нас от множественного наследования, однако все же требует некоторых знаний о слушателе.
Мы же с вами рассмотрим «чистую» систему событий (events system) – не требующую знаний о том кто будет источником/получателем нащих событий, и есть ли таковые вообще. То есть посылающий события вообще ничего не знает о том куда они уходят и что с этой информацией будут делать. Эдакая система коллбеков 80-го уровня ;)
До недавнего времени для построения такой системы требовалось включение тяжелых библиотек а-ля Boost и целого вороха макросов. С появлением стандарта С++11 жить стало проще, и теперь реализовать такую систему можно в пару десятков строчек чистого С++ кода.
Примечание: расмотренная ниже система используется автором в нескольких небольших домашних проектах, и полностью удовлетворяет оного как скоростью, так и удобством. Если вы захотите использовать приведенный код в своих проектах – помните, автор не несет никакой ответственности за вред/срыв сроков/кранчи/увольнения причененные данным кодом. Используйте на свой страх и риск, и с благословления ближайшей к вам церкви.
Наша событийная система базируется на 2-х фичах нового С++11 стандарта – std::function и variadic templates. Предполагается у читателя базовых знаний о С++11 и данных его фичах.
std::function [4] – это практически клон boost::function, и в свою очередь является высокоуровневой оберткой над функциями и функторами. Для захвата методов классов мы будем использовать std::bind.
Variadic templates (или parameters pack) [5] – фича, которую многие (включая меня) давно ждали, и которая пришла из замечательного языка D, где ее возможности еще шире.
Суть данной фичи в том, чтобы шаблоны могли принимать переменное количество аргументов. Типичный пример variadic template – std::tuple – контейнер фиксированного размера, способный содержать элементы различных типов.
template <typename... Params> class tuple;
Для «распаковки» аргументов был введен оператор ... (троеточие). При этом все разделительные запятые будут проставлены автоматически. Кто пробовал реализовать потодобно на макросах знают сколько кода приходилось писать для этого.
Для того чтобы узнать количество аргументов был введен оператор sizeof...().
Теперь перейдем к деталям реализации.
Главный класс – EventSystem – держит все зарегистрированные события в контейнере типа std::unordered_map, так как частые вставки-удаления не планируются, но нам нужна быстрая выборка по ключу. Данный класс является синглтоном (Singleton) [6]
Основные 4 метода класса (все шаблонные, в качестве шаблонного параметра принимают специальную структуру-описатель события (EventTrait)):
• RegisterEvent – регистрирует событие в системе событий.
• SubscribeToEvent – подписывает объект на событие. В качестве параметров принимает указатель на объект класса и метод класса, который будет реагировать на событие.
• UnsibscribeFromEvent – отписывает объект от события. В качестве параметра принимает указатель на объект класса.
• RaiseEvent – вызывает событие, по цепочке будут оповещены все подписавшиеся. Все полученные аргументы будут переданы подписчикам (за это отвечает std::forward).
Для визуальной красоты рекомендуется пользоваться функциями-помощниками:
• ev::register_event
• ev::subscribe_event
• ev::unsunscribe_event
• ev::raise_event
Для хранения событий в контейнере все события наследуются от общего абстрактного класса BasicEvent не имеющего методов.
Класс SpecEvent является специализированным шаблонным классом для событий, специализатором для него является все та же структура-описатель EventTrait.
Основные методы:
• Bind – производит захват метода класса и указателя на объект класса и сохраняет их в списке подписчиков.
• Unbind – удаляет объект класса из списка слушателей.
• Call – вызывает по цепочке сохраненные методы классов-слушателей передавая им аргументы. Для того чтобы не терять аттрибуты аргументов (особенно для ссылочных аргументов) – используется move-семантика [7]
Как уже говорилось ранее – для захвата методов класса используется std::bind, которая вернет полноценный объект-фунцию. Использование std::bind необходимо так же для захвата первого, неявного, параметра метода-функции – указателя на объект класса. Для того чтобы опустить захват остальных аргументов мы используем т.н. placeholders [8].
И все бы хорошо, однако мы в момент биндинга должны передавать определенное количество плейсхолдеров. Можно, конечно, написать специализированные биндинг-функции для каждого количества аргументов, а можно использовать возможности variadic templates.
Для этого напишем свою последовательность заглушек используя самописный аналог integer_sequence из C++14. Реализация подсмотрена в http://loungecpp.wikidot.com/tips-and-tricks%3aindices