4
\$\begingroup\$

I am working on a simple event mechanism for c++. Since Iam not very experienced I would like to share my code in order to get some thoughts.

I also dont like the BIND_X macros. Ideas how to simplify the binding process are welcome as well any comments regarding problems, bad style...

#ifndef EVENTS_H #define EVENTS_H #include <functional> #include <algorithm> #include <random> #include <limits> #include <string> #include <forward_list> #include <mutex> namespace event { // The type of the source identifier passed to each event listener typedef const std::string& source_t; /// /// \brief A handle that identifiers listeners. /// /// \details Listeners are functions that get called once a event is fired. /// This struct is used to identify such functions and is used to detach them. /// struct listener_handle { public: /// /// \brief Create a new handle /// \param s The source /// \param h The handle id /// listener_handle(source_t s="", int h=0) : source(s), handle(h) { } /// /// \brief Equals operator /// \param other The handle to compare /// \return True, if the handles are equal /// bool operator==(const listener_handle& other) const { return this->source == other.source && this->handle == other.handle; } std::string source; int handle; }; template <class... T> /// /// \brief The event class. /// class Event { public: typedef std::function<void(source_t, T...)> func; /// /// \brief Create new instance /// \param source The name of the event source. /// Event(source_t source) : source(source) {} /// /// \brief Release resources /// virtual ~Event() { this->listeners.clear(); } /// /// \brief Attach an event /// \param newListener The event listener to attach /// \return The handle that may be used to detach the event /// virtual listener_handle& Attach(const func& newListener) { this->listeners.push_front(Listener{newListener, this->createListenerHandle()}); return this->listeners.front().handle; } /// /// \brief Detach an event using its id /// \param id The id of the event to detach /// virtual void Detach(const listener_handle& handle) { this->listeners.remove_if([handle] (const Listener& l) {return l.handle == handle;}); } /// /// \brief Call all listeners /// \param argument The EventArgs to send /// virtual void Invoke(const T&... args) const { std::for_each(std::begin(this->listeners), std::end(this->listeners), [this, &args...] (const Listener& l) { l.listener(this->source, args...); }); } private: struct Listener { func listener; listener_handle handle; }; /// /// \brief Create a random number using per thread local seed. /// \return A random number between int min and int max /// int createRandom() const { static std::mt19937 gen{std::random_device{}()}; static std::uniform_int_distribution<> dist{ std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}; return dist(gen); } /// /// \brief Create a new listener handle using the registered source name /// \return A new listener handle /// listener_handle createListenerHandle() const { return listener_handle{this->source, this->createRandom()}; } std::string source; std::forward_list<Listener> listeners; }; template <typename... T> /// /// \brief The thread safe event class. /// /// \details This class should be used if the exposed event may be accessed from multiple threads. /// class TsEvent : public Event<T...> { public: /// /// \copydoc Event::Event() /// TsEvent(source_t source): Event<T...>(source) { } /// /// \copydoc Event::~Event() /// virtual ~TsEvent() { } /// /// \copydoc Event::Attach() /// virtual listener_handle& Attach(const typename Event<T...>::func& newListener) override { std::lock_guard<std::mutex> lg(this->m); return Event<T...>::Attach(newListener); } /// /// \copydoc Event::Detach() /// virtual void Detach(const listener_handle& handle) override { std::lock_guard<std::mutex> lg(this->m); return Event<T...>::Detach(handle); } /// /// \copydoc Event::Invoke() /// virtual void Invoke(const T&... args) const override { std::lock_guard<std::mutex> lg(this->m); return Event<T...>::Invoke(args...); } private: std::mutex m; }; } //event namespace #endif // EVENTS_H 

Sample usage (assume CallMe is a static function with 2 parameter):

#include <string> #include <iostream> #include "event.hpp" void CallMe(std::string s, int i) { std::cout << s << "-" << i << '\n'; } int main() { auto t = new event::Event<int>{"Basic"}; auto handle = t->Attach(std::function<void(std::string, int)>{CallMe}); t->Invoke(5); t->Detach(handle); delete t; } 

Note: The bind macros are used to simplify binding of methods that require some kind of instance so call. Assuming a class s exposes an event e that requires 3 parameter and callMe (defined inside receiver) satisfies this, one may use it like that:

auto handle = s->e->Attach(BIND_3(receiver::callMe, r)); 

Edit: Here is an example of how to avoid the BIND_X macros (and that is why i got rid if them)

auto handle = s->e->Attach([r](const std::string& s, int a, int b) {r->callMe(s, a, b);}); 
\$\endgroup\$

    2 Answers 2

    2
    \$\begingroup\$

    Using std::function you don't ensure signature matching (It work if argument are just convertible). some kind of function_traits can be applied here.

    Try to check at compile time that the signature of the given callback match what you expect.

    The loop in Invoke can be simplified by using a range-based for loop or std::for_each.

    You don't have to write this-> everywhere.

    Did you consider using a queue instead of a list? Semantically, it's more correct

    The UniqueId generation can be outed from this class, since it's a different concern and maybe reusable.

    For your "bind" problem, as callback, user can pass a lambda capturing the instance.

    note: I modified your "sample" to get it in a working example, but didn't fix the memory leak present in your code original. new and delete are as an old couple, they never go out without each other.

    edit : I would not have made it possible to inherit the Event class. If it was my class, I would rather opt for a tagged_type so that each event is of a different type, even if signatures matches, even if signatures matches. But this desing choice is a matter of taste.

    If you want a better understanding of function_traits you can check this.

    \$\endgroup\$
    3
    • \$\begingroup\$I will look into some way to implement function traits. I dont think a queue is the right data structure since any listener may detach their event at any time. So storing them in a queue doesnt make sense to me. I updated the above code according to your suggestions. (I keep using this-> since it makes it easy to deduce the scope of a variable.)\$\endgroup\$
      – Peepe
      CommentedNov 2, 2018 at 19:42
    • 1
      \$\begingroup\$Be careful about rules: What should I do when someone answers my question?\$\endgroup\$
      – Calak
      CommentedNov 2, 2018 at 20:17
    • \$\begingroup\$@Peepe I edited my answer. You'r right for queues, I didn't notice this. Try now to outsource the random generator and think about function_traits and my edit. And of course, if I answered your question, do not forget to eventually tick my answer as accepted.\$\endgroup\$
      – Calak
      CommentedNov 2, 2018 at 21:31
    2
    \$\begingroup\$

    Took a while but a got the function traits working. To make the check actually work I had to remove the inherit argument type from the Attach function and make it a template function itself. (Type conversion would take place before the static_assert inside the function happens)

    Maybe I will remove the yet unused part of the function_traits (arity, return type). Not sure about that yet.

    event.hpp:

    namespace event { // The type of the source identifier passed to each event listener typedef const std::string& source_t; template<typename... args> using func = std::function<void(source_t, args...)>; /// /// \brief A handle that identifiers listeners. /// /// \details Listeners are functions that get called once a event is fired. /// This struct is used to identify such functions and is used to detach them. /// struct listener_handle { public: /// /// \brief Create a new handle /// \param s The source /// \param h The handle id /// listener_handle(source_t s="", int h=0) : source(s), handle(h) { } /// /// \brief Equals operator /// \param other The handle to compare /// \return True, if the handles are equal /// bool operator==(const listener_handle& other) const { return this->source == other.source && this->handle == other.handle; } std::string source; int handle; }; template <class... T> /// /// \brief The event class. /// class Event { public: /// /// \brief Create new instance /// \param source The name of the event source. /// Event(source_t source) : source(source) {} /// /// \brief Release resources /// ~Event() { this->listeners.clear(); } /// /// \brief Attach an event /// \param newListener The event listener to attach /// \return The handle that may be used to detach the event /// template <class... args> listener_handle& Attach(const func<args...>& newListener) { using listener_traits = ftraits::function_traits<typename std::decay<decltype(newListener)>::type>; ftraits::assert_traits_equal<listener_traits, traits>(); this->listeners.push_front(Listener{newListener, this->createListenerHandle()}); return this->listeners.front().handle; } /// /// \brief Detach an event using its id /// \param id The id of the event to detach /// void Detach(const listener_handle& handle) { this->listeners.remove_if([handle] (const Listener& l) {return l.handle == handle;}); } /// /// \brief Call all listeners /// \param argument The EventArgs to send /// void Invoke(const T&... args) const { std::for_each(std::begin(this->listeners), std::end(this->listeners), [this, &args...] (const Listener& l) { l.listener(this->source, args...); }); } private: using traits = ftraits::function_traits<func<T...>>; struct Listener { func<T...> listener; listener_handle handle; }; /// /// \brief Create a new listener handle using the registered source name /// \return A new listener handle /// listener_handle createListenerHandle() const { return listener_handle{this->source, createRandom()}; } std::string source; std::forward_list<Listener> listeners; }; } //event namespace 

    function_traits.hpp:

    namespace ftraits { /// /// \brief Undefined base /// template<typename> struct function_traits; /// /// \brief Specialization for std::function /// template <typename Function> struct function_traits : public function_traits<decltype(&Function::operator())> { }; /// /// \brief Function traits implementation /// template <typename ClassType, typename ReturnType, typename... Arguments> struct function_traits<ReturnType(ClassType::*)(Arguments...) const> { typedef ReturnType result_type; static constexpr std::size_t arity = sizeof...(Arguments); template <std::size_t N> struct argument { using type = typename std::tuple_element<N, std::tuple<Arguments...>>::type; }; }; template<typename t1, typename t2> /// /// \brief Check the the function traits are equal using static assert /// void assert_traits_equal() { static_assert(std::is_same<t1, t2>::value, "The function signatures do not match."); }; } #endif // FUNCTION_TRAITS_H 

    I also moved the random number generator to a separate header file.

    \$\endgroup\$
    1
    • \$\begingroup\$Great, I really like what it's become!\$\endgroup\$
      – Calak
      CommentedNov 4, 2018 at 17:54

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.