3

I want to create an event system that uses lambda functions as its subscribers/listeners, and an event type to assign them to the specific event that they should subscribe to. The lambdas should have variable arguments, as different kinds of events use different kinds of arguments/provide the subscribers with different kinds of data.

For my dispatcher, I have the following:

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;
};

No issues here, but when I go to implement the subscribe() function in my .cpp file, like this:

void EventDispatcher::subscribe(EventType event_type, std::function<void(...)> callback) {
    ... (nothing here yet)
}

The IDE shows me this:

Implicit instantiation of undefined template 'std::function<void (...)>'

0

4 Answers 4

3

You could create your own version of std::function that accepts functions of any signature using type erasure. This will require some heavy lifting though. I will provide a solution for void functions which requires C++17, because we will use std::any.

I will walk you through the steps first and then provide a full solution in code.

  • First you create some function_traits that capture the number and type of arguments of any kind of function using template meta-programming. We can "borrow" from here.
  • Then you create a class VariadicVoidFunction that has a templated call operator.
  • This call operator creates a std::vector<std::any> and passes it to the invoke method of a member of VariadicVoidFunction, which is a (resource-owning smart) pointer of type VariadicVoidFunction::Concept.
  • VariadicVoidFunction::Concept is an abstract base class with a virtual invoke method that accepts std::vector<std::any>.
  • VariadicVoidFunction::Function is a class template, where the template parameter is a function. It stores this function as member and inherits VariadicVoidFunction::Concept. It implements the invoke method. Here, we can std::any_cast the vector elements back to the expected types, which we can extract using function_traits. This allows us to call the actual function with the correct argument types.
  • VariadicVoidFunction gets a templated constructor accepting any kind of function F. It creates an instance of type VariadicVoidFunction::Function<F> and stores it in an owning (smart) pointer.
#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;

};

You can use it like this:

#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

Demo on Godbolt Compiler explorer

Note that this is a prototypical solution to get you started. One downside is that all arguments will be copied into the std::any You could avoid this by passing pointers to std::any, but you have to be careful with lifetimes when you do this.

Sign up to request clarification or add additional context in comments.

1 Comment

On a side node, VariadicVoidFunction is a bad choice for a name, because it isn't really variadic (the number of arguments must match the number of arguments of the function passed to the ctor) and you can't use variadic functions with it (the type_traits aren't powerful enough for this). You can only model arbitrary functions with a fixed number of arguments each under the same type.
2

Don't try to plop event callbacks with different parameter types directly into a single map.

Instead, create a template to store the callback (templated by the parameter types), and store pointers to its non-template base.

Here's how I would do it:

#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();
}

Comments

1

After the input from @joergbrech and @HolyBlackCat I made this

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;
};

I've tested it and I get the expected result from this solution. For the full code solution -> https://github.com/F4LS3/forge-engine

Comments

0

std::function has no specialization for variadic function types.

You likely want std::function<void()>.

2 Comments

Will this allow me to use different arguments inside the lambda?
No. (Neither will a variadic function portably if the arguments are non-trivial.) Usually you would expect a fixed set of types for a particular event type so I'd use those as the argument types. Logically, it's the same as including those within the event type.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.