Я хочу создать систему событий, которая использует лямбда-функции в качестве своих подписчиков/слушателей и тип события, чтобы назначать их конкретному событию, на которое они должны подписаться. Лямбда-выражения должны иметь переменные аргументы, поскольку разные типы событий используют разные типы аргументов/предоставляют подписчикам разные типы данных.
Для моего диспетчера у меня есть следующее:
class EventDispatcher {
public:
static void subscribe(EventType event_type, std::function<void(...)> callback);
void queue_event(Event event);
void dispatch_queue();
private:
std::queue<Event*> event_queue;
std::map<EventType, std::function<void(...)>> event_subscribers;
};
Здесь нет проблем, но когда я перехожу к реализации функции subscribe()
в моем файле .cpp
, вот так:
void EventDispatcher::subscribe(EventType event_type, std::function<void(...)> callback) {
... (nothing here yet)
}
IDE показывает мне это:
Неявное создание неопределенного шаблона 'std::function<void (...)>'
std::function
не имеет специализации для функций с переменным числом переменных.
Вы, вероятно, хотите std::function<void()>
.
Нет. (Вариадная функция также не будет переносимой, если аргументы нетривиальны.) Обычно вы ожидаете фиксированный набор типов для определенного типа события, поэтому я бы использовал их в качестве типов аргументов. Логически это то же самое, что и включение в тип события.
Не пытайтесь помещать обратные вызовы событий с различными типами параметров непосредственно в одну карту.
Вместо этого создайте шаблон для хранения обратного вызова (шаблона по типам параметров) и сохраните указатели на его базу, не являющуюся шаблоном.
Вот как бы я это сделал:
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <queue>
#include <tuple>
#include <typeindex>
#include <typeinfo>
#include <type_traits>
#include <utility>
struct Event
{
virtual ~Event() = default;
};
struct Observer
{
virtual ~Observer() = default;
virtual void Observe(const Event &e) const = 0;
};
template <typename ...P>
struct BasicEvent : Event
{
std::tuple<P...> params;
BasicEvent(P ...params) : params(std::move(params)...) {}
struct EventObserver : Observer
{
std::function<void(P...)> func;
template <typename T>
EventObserver(T &&func) : func(std::forward<T>(func)) {}
void Observe(const Event &e) const override
{
std::apply(func, dynamic_cast<const BasicEvent &>(e).params);
}
};
// We need a protected destructor, but adding one silently removes the move operations.
// And adding the move operations removes the copy operations, so we add those too.
BasicEvent(const BasicEvent &) = default;
BasicEvent(BasicEvent &&) = default;
BasicEvent &operator=(const BasicEvent &) = default;
BasicEvent &operator=(BasicEvent &&) = default;
protected:
~BasicEvent() {}
};
class EventDispatcher
{
public:
template <typename E>
void Subscribe(typename E::EventObserver observer)
{
event_subscribers.insert_or_assign(typeid(E), std::make_unique<typename E::EventObserver>(std::move(observer)));
}
template <typename E>
void QueueEvent(E &&event)
{
event_queue.push(std::make_unique<std::remove_cvref_t<E>>(std::forward<E>(event)));
}
void DispatchQueue()
{
while (!event_queue.empty())
{
Event &event = *event_queue.front();
event_subscribers.at(typeid(event))->Observe(event);
event_queue.pop();
}
}
private:
std::queue<std::unique_ptr<Event>> event_queue;
std::map<std::type_index, std::unique_ptr<Observer>> event_subscribers;
};
struct EventA : BasicEvent<> {using BasicEvent::BasicEvent;};
struct EventB : BasicEvent<> {using BasicEvent::BasicEvent;};
struct EventC : BasicEvent<int, int> {using BasicEvent::BasicEvent;};
int main()
{
EventDispatcher dis;
dis.Subscribe<EventA>([]{std::cout << "Observing A!\n";});
dis.Subscribe<EventB>([]{std::cout << "Observing B!\n";});
dis.Subscribe<EventC>([](int x, int y){std::cout << "Observing C: " << x << ", " << y << "!\n";});
dis.QueueEvent(EventA());
dis.QueueEvent(EventB());
dis.QueueEvent(EventC(1, 2));
dis.DispatchQueue();
}
Вы можете создать свою собственную версию std::function
, которая принимает функции любой подписи, используя стирание типа. Однако для этого потребуется тяжелая работа. Я предоставлю решение для пустых функций, которое требует C++17, потому что мы будем использовать std::any
.
Сначала я проведу вас по шагам, а затем предоставлю полное решение в коде.
function_traits
, которые фиксируют количество и тип аргументов любой функции, используя метапрограммирование шаблонов. Мы можем «позаимствовать» у здесь.VariadicVoidFunction
с шаблонным оператором вызова.std::vector<std::any>
и передает его методу invoke
члена VariadicVoidFunction
, который является (интеллектуальным) указателем типа VariadicVoidFunction::Concept
.VariadicVoidFunction::Concept
— это абстрактный базовый класс с виртуальным invoke
методом, который принимает std::vector<std::any>
.VariadicVoidFunction::Function
— это шаблон класса, где параметр шаблона — это функция. Он сохраняет эту функцию как элемент и наследует VariadicVoidFunction::Concept
. Он реализует метод invoke
. Здесь мы можем std::any_cast
вернуть векторным элементам ожидаемые типы, которые мы можем извлечь с помощью function_traits
. Это позволяет нам вызывать фактическую функцию с правильными типами аргументов.VariadicVoidFunction
получает шаблонный конструктор, принимающий любую функцию F
. Он создает экземпляр типа VariadicVoidFunction::Function<F>
и сохраняет его в владеющем (интеллектуальном) указателе.#include <memory>
#include <vector>
#include <any>
// function_traits and specializations are needed to get arity of any function type
template<class F>
struct function_traits;
// ... function pointer
template<class R, class... Args>
struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)>
{};
// ... normal function
template<class R, class... Args>
struct function_traits<R(Args...)>
{
static constexpr std::size_t arity = sizeof...(Args);
template <std::size_t N>
struct argument
{
static_assert(N < arity, "error: invalid parameter index.");
using type = typename std::tuple_element<N,std::tuple<Args...>>::type;
};
};
// ... non-const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...)> : public function_traits<R(C&,Args...)>
{};
// ... const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...) const> : public function_traits<R(C const&,Args...)>
{};
// ... functor (no overloads allowed)
template<class F>
struct function_traits
{
private:
using call_type = function_traits<decltype(&F::operator())>;
public:
static constexpr std::size_t arity = call_type::arity - 1;
template <std::size_t N>
struct argument
{
static_assert(N < arity, "error: invalid parameter index.");
using type = typename call_type::template argument<N+1>::type;
};
};
template<class F>
struct function_traits<F&> : public function_traits<F>
{};
template<class F>
struct function_traits<F&&> : public function_traits<F>
{};
// type erased void function taking any number of arguments
class VariadicVoidFunction
{
public:
template <typename F>
VariadicVoidFunction(F const& f)
: type_erased_function{std::make_shared<Function<F>>(f)} {}
template <typename... Args>
void operator()(Args&&... args){
return type_erased_function->invoke(std::vector<std::any>({args...}));
}
private:
struct Concept {
virtual ~Concept(){}
virtual void invoke(std::vector<std::any> const& args) = 0;
};
template <typename F>
class Function : public Concept
{
public:
Function(F const& f) : func{f} {}
void invoke(std::vector<std::any> const& args) override final
{
return invoke_impl(
args,
std::make_index_sequence<function_traits<F>::arity>()
);
}
private:
template <size_t... I>
void invoke_impl(std::vector<std::any> const& args, std::index_sequence<I...>)
{
return func(std::any_cast<typename function_traits<F>::template argument<I>::type>(args[I])...);
}
F func;
};
std::shared_ptr<Concept> type_erased_function;
};
Вы можете использовать его следующим образом:
#include <unordered_map>
#include <iostream>
int main()
{
VariadicVoidFunction([](){});
std::unordered_map<size_t, VariadicVoidFunction> map =
{
{0, VariadicVoidFunction{[](){ std::cout << "no argument\n"; }} },
{1, VariadicVoidFunction{[](int i){ std::cout << "one argument\n"; }} },
{2, VariadicVoidFunction{[](double j, const char* x){ std::cout<< "two arguments\n"; }} }
};
map.at(0)();
map.at(1)(42);
map.at(2)(1.23, "Hello World");
return 0;
}
no argument
one argument
two arguments
Демонстрация в проводнике Godbolt Compiler
Обратите внимание, что это прототип решения для начала работы. Единственным недостатком является то, что все аргументы будут скопированы в std::any
Этого можно избежать, передав указатели на std::any, но при этом нужно быть осторожным с временем жизни.
На побочном узле VariadicVoidFunction
— плохой выбор для имени, потому что на самом деле оно не является вариативным (количество аргументов должно совпадать с количеством аргументов функции, переданной в ctor), и вы не можете использовать с ним функции с вариативностью. (type_traits недостаточно мощны для этого). Вы можете моделировать только произвольные функции с фиксированным числом аргументов, каждая из которых относится к одному и тому же типу.
После комментариев от @joergbrech и @HolyBlackCat я сделал это
enum class EventType {
WindowClosed, WindowResized, WindowFocused, WindowLostFocus, WindowMoved,
AppTick, AppUpdate, AppRender,
KeyPressed, KeyRelease,
MouseButtonPressed, MouseButtonRelease, MouseMoved, MouseScrolled,
ControllerAxisChange, ControllerButtonPressed, ControllerConnected, ControllerDisconnected
};
class IEvent {
public:
IEvent(EventType event_type) {
this->event_type = event_type;
}
EventType get_event_type() {
return event_type;
}
private:
EventType event_type;
};
class IEventSubscriber {
public:
/**
* @param event The event that is passed to the subscriber by the publisher; should be cast to specific event
* */
virtual void on_event(IEvent *event) = 0;
EventType get_event_type() {
return event_type;
}
protected:
explicit IEventSubscriber(EventType event_type) {
this->event_type = event_type;
}
private:
EventType event_type;
};
class FORGE_API EventPublisher {
public:
static void subscribe(IEventSubscriber *subscriber);
static void queue_event(IEvent *event);
static void dispatch_queue();
private:
static std::queue<IEvent*> event_queue;
static std::set<IEventSubscriber*> event_subscribers;
};
Я протестировал его и получил ожидаемый результат от этого решения. Для решения полного кода -> https://github.com/F4LS3/forge-engine
Позволит ли это мне использовать разные аргументы внутри лямбды?