Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Event emitter and automatic registrations of member methods as listeners

This is a question with answer, the aim of which is to invite the readers to suggest their own solutions.
I'm quite sure that out there there are more clever approaches than mine, so I'd like to know what those solutions are.
Please, share your knowledge by adding your own answers!!


The goal is to create an emitter class that can be used to dispatch a few events.

An important feature I want to be in the emitter is an easy to use registration facility to attach the listeners to the emitter.

In other terms, I don't want to write functions/methods aimed to attach all the listeners to the emitter, for it could be error prone and I found myself more than once looking for a bug that was due to a missed line of code because of that (a line that would have registered the N-th listener, of course).

Imagine the following structs:

struct E1 { };

struct S {
    void receive(const E1 &ev) { /* do something */ }
};

The registration facility I'm looking for is such a solution for which the following lines of code are enough:

S s;
emitter.reg(s);

And that's all, even if in the future it arises the requirement for one more listener to be added to the struct S, as an example:

struct E1 { };
struct E2 { };

struct S {
    void receive(const E1 &ev) { /* do something */ }
    void receive(const E2 &ev) { /* do something */ }
};

How could I write such an emitter?

like image 399
skypjack Avatar asked Apr 16 '16 16:04

skypjack


2 Answers

First the includes:

#include <iostream>
#include <vector>
#include <type_traits>
#include <utility>
#include <functional>

We use the void_t detection helper:

template<class ...>
using void_t = void;

We define a trait for detecting the receive() methods using void_t:

template<class C, class E, class X = void_t<>>
struct has_event_handler :
      std::false_type {};

template<class C, class E>
struct has_event_handler<C, E, void_t< decltype(
    std::declval<C>().receive(std::declval<const E>())
) >> : std::true_type {};

template<class C, class E>
constexpr bool has_event_handler_v = has_event_handler<C, E>::value;

Using this, we can define the emitter class. The variadic arguments are the type of events it can manage:

template<class...> class Emitter;

// Recursive case:
template<class E, class... F>
class Emitter<E, F...> : Emitter<F...> {
public:
  // Register:
  template<class C>
  std::enable_if_t<!has_event_handler_v<C,E>> reg(C& callback) {
    Emitter<F...>::reg(callback);
  };
  template<class C>
  std::enable_if_t<has_event_handler_v<C,E>> reg(C& callback) {
    handlers_.push_back([&callback](E const& event) { return callback.receive(event); });
    Emitter<F...>::reg(callback);
  };
  void trigger(E const& event)
  {
    for (auto const& handler : handlers_)
      handler(event);    
  }
  template<class G>
  void trigger(G const& event)
  {
    Emitter<F...>::trigger(event);
  }
private:
  std::vector<std::function<void(const E&)>> handlers_;
};

// Base case:
template<>
class Emitter<> {
public:
  template<class C>
  void reg(C& callback) {};
  template<class E>
  void trigger(E const& event)
  {
     static_assert(!std::is_same<E,E>::value,
       "Does not handle this type of event.");
  }
};

For the trigger() part, another solution would be to use std::enable_if_t<std::is_base_of_v<E, G>>.

And we can use it with:

// Events
struct E1 {};
struct E2 {};
struct E3 {};

// Handler
struct handler {
  void receive(const E1&)
  {
    std::cerr << "E1\n";
  }
  void receive(const E2&)
  {
    std::cerr << "E2\n";
  }
};

// Check the trait:
static_assert(has_event_handler_v<handler, E1>, "E1");
static_assert(has_event_handler_v<handler, E2>, "E2");
static_assert(!has_event_handler_v<handler, E3>, "E3");

int main()
{
  Emitter<E1, E2> emitter;
  handler h;
  emitter.reg(h);
  emitter.trigger(E1());
  emitter.trigger(E2());
}

Note: I used the _v and _t variants from C++17 in order to have a shorter code but for compatibility with C++11 you might want to use the struct versions (typename std::enable_if<foo>::type, std::is_base_of<B,D>::value, etc.).

Update: it's probably better to use composition instead of inheritance for the recursive case of Emitter:

template<class E, class... F>
class Emitter<E, F...> {
public:
  // Register:
  template<class C>
  std::enable_if_t<!has_event_handler_v<C,E>> reg(C& callback) {
    Emitter<F...>::reg(callback);
  };
  template<class C>
  std::enable_if_t<has_event_handler<C,E>::value> reg(C& callback) {
    handlers_.push_back([&callback](E const& event) { return callback.receive(event); });
    emitter_.reg(callback);
  };
  void trigger(E const& event)
  {
    for (auto const& handler : handlers_)
      handler(event);    
  }
  template<class G>
  void trigger(G const& event)
  {
    emitter_.trigger(event);
  }
private:
  std::vector<std::function<void(const E&)>> handlers_;
  Emitter<F...> emitter_;
};
like image 148
ysdx Avatar answered Oct 26 '22 22:10

ysdx


My version without inheritance:

template <typename C, typename E> std::false_type has_event_handler_impl(...);
template <typename C, typename E>
auto has_event_handler_impl(int)
-> decltype(static_cast<void>(std::declval<C>().receive(std::declval<const E>())),
            std::true_type{});

template <typename C, typename E>
using has_event_handler = decltype(has_event_handler_impl<C, E>(0));

template <class... Es>
class Emitter {
public:

    template<class C>
    void reg(C& callback) {
        const int dummy[] = { 0, (regT<Es>(callback), 0)...};
        static_cast<void>(dummy); // Avoid unused variable warning
    }

    template <typename E>
    void emit(const E& event)
    {
        for (auto const& handler : get_vector<E>()) {
            handler(event);
        }
    }

private:
    template <typename E, typename C>
    std::enable_if_t<has_event_handler<C, E>::value>
    regT(C& callback)
    {
        auto lambda = [&callback](const E& event) { return callback.receive(event); };
        get_vector<E>().push_back(lambda);
    }

    template <typename E, typename C>
    std::enable_if_t<!has_event_handler<C, E>::value>
    regT(C&)
    {
        /* Empty */
    }

    template <typename E>
    std::vector<std::function<void(const E&)>>& get_vector()
    {
        return std::get<std::vector<std::function<void(const E&)>>>(handlers_);
    }

private:
    std::tuple<std::vector<std::function<void(const Es&)>>...> handlers_;
};

Demo

like image 27
Jarod42 Avatar answered Oct 26 '22 23:10

Jarod42