11
\$\begingroup\$

I've created a simplified event system for C++14. The code is commented, so it shouldn't be hard to read. There's also a simple usage scenario below.

It is still a work in progress and uses some not-so-best practices. For example any consumer, that has an access to the event can fire it. Also you will notice, that the subscribers (event handlers) are put into an unordered_map, so they need a unique key, by which we could later determine, whether such a handler already exists. I get this key by 'unionizing' the function object with a uint64_t, multiplying by 10 and then by adding the instance address. I doubt it is a very good idea and platform independent.

Any ideas and critique is welcome.

#ifndef __PD_EVENT__
#define __PD_EVENT__
#define PD_EVENT_VER 8
#include <functional>
#include <iostream>
#include <stdint.h>
#include <unordered_map>
using namespace std::placeholders;
//
// @brief Describes additional parameters for handler binding.
//
enum class EventFlag {
 DEFAULT = 0,
 ONLY_UNIQUE = 1
};
//
// @version 8
// @brief Defines means for delegate function subscription and calling on demand.
// Maximum allowed number of event arguments is 4.
// @param
//
template<class... Args>
class Event{
 //
 // @brief Address representation for use as identifier in container mapping.
 //
 using Address = uint64_t;
 //
 // @brief Function object with user-defined variadic template arguments.
 //
 using Handler = std::function<void(Args...)>;
 //
 // @brief Type that provides a unique memory-based integral definition for given objects.
 // Concept: should be collision-free.
 //
 using Identifier = uint64_t;
 //
 // @brief Maximum supported event handler arguments.
 //
 static constexpr auto _MAX_EVENT_ARGS = 4;
 static_assert(sizeof...(Args) <= _MAX_EVENT_ARGS, "Too many arguments");
public:
 //
 // @brief Subscribes a lambda expression with a matching argument list.
 // @param lambda - the lambda expression to bind.
 //
 void operator+=(Handler lambda) {
 // TODO find a way to unbind
 _addToList(_identify(&lambda, 0), lambda, EventFlag::DEFAULT);
 }
 //
 // @brief Doubles as a fire function.
 //
 void operator()(Args... e) {
 fire(e...);
 }
 //
 // @brief Calls every subscriber in container with given arguments.
 // @param e... - arguments, that the defined event accepts.
 // Contract: [0-4] arguments
 //
 void fire(Args... e) {
 for (auto subscriber : subscribers)
 subscriber.second(e...);
 }
 //
 // @brief Checks if there's a subscriber with a matching address in the container.
 //
 bool hasSubscriber(Address a) const {
 return subscribers.find(a) != subscribers.end();
 }
#pragma mark - bind(...) overloads
 //
 // @brief Subscribes a member function with no arguments.
 // @param member - member function pointer &C::M.
 // @param instance - instance of structure housing the member function.
 // @usage <i>event.bind(&MyClass::member, myClassInstance)</i>.
 //
 template<class C, class M, class T>
 void bind(C (M::*member)(), T *instance, EventFlag flag = EventFlag::DEFAULT) {
 _addToList(_identify(instance, member), std::bind(member, instance), flag);
 }
 //
 // @brief Subscribes a member function with 1 argument.
 // Event handler argument A1.
 //
 template<class C, class M, class T, typename A1>
 void bind(C (M::*member)(A1), T *instance, EventFlag flag = EventFlag::DEFAULT) {
 _addToList(_identify(instance, member), std::bind(member, instance, _1), flag);
 }
 //
 // @brief Subscribes a member function with 2 arguments.
 // Event handler arguments: A1, A2.
 //
 template<class C, class M, class T, typename A1, typename A2>
 void bind(C (M::*member)(A1, A2), T *instance, EventFlag flag = EventFlag::DEFAULT) {
 _addToList(_identify(instance, member), std::bind(member, instance, _1, _2), flag);
 }
 //
 // @brief Subscribes a member function with 3 arguments.
 // Event handler arguments: A1, A2, A3.
 //
 template<class C, class M, class T, typename A1, typename A2, typename A3>
 void bind(C (M::*member)(A1, A2, A3), T *instance, EventFlag flag = EventFlag::DEFAULT) {
 _addToList(_identify(instance, member), std::bind(member, instance, _1, _2, _3), flag);
 }
 //
 // @brief Subscribes a member function with 4 arguments.
 // Event handler arguments: A1, A2, A3, A4.
 //
 template<class C, class M, class T, typename A1, typename A2, typename A3, typename A4>
 void bind(C (M::*member)(A1, A2, A3, A4), T *instance, EventFlag flag = EventFlag::DEFAULT) {
 _addToList(_identify(instance, member), std::bind(member, instance, _1, _2, _3, _4), flag);
 }
#pragma mark - unbind(...) overloads
 //
 // @brief Unsubscribes a member function by its function pointer address as key.
 // @param member - member function pointer &C::M.
 // @param instance - instance of structure housing the member function.
 // @usage <i>event.unbind(&MyClass::member, myClassInstance)</i>.
 //
 template<typename C, typename M, typename T>
 void unbind(C (M::*member)(), T *instance) {
 _removeFromList(_identify(instance, member));
 }
 //
 // @brief Unsubscribes a member function by its function pointer address as key.
 // Event handler argument A1.
 //
 template<class C, class M, class T, typename A1>
 void unbind(C (M::*member)(A1), T *instance) {
 _removeFromList(_identify(instance, member));
 }
 //
 // @brief Unsubscribes a member function by its function pointer address as key.
 // Event handler arguments: A1, A2.
 //
 template<class C, class M, class T, typename A1, typename A2>
 void unbind(C (M::*member)(A1, A2), T *instance) {
 _removeFromList(_identify(instance, member));
 }
 //
 // @brief Unsubscribes a member function by its function pointer address as key.
 // Event handler arguments: A1, A2, A3.
 //
 template<class C, class M, class T, typename A1, typename A2, typename A3>
 void unbind(C (M::*member)(A1, A2, A3), T *instance) {
 _removeFromList(_identify(instance, member));
 }
 //
 // @brief Unsubscribes a member function by its function pointer address as key.
 // Event handler arguments: A1, A2, A3, A4.
 //
 template<class C, class M, class T, typename A1, typename A2, typename A3, typename A4>
 void unbind(C (M::*member)(A1, A2, A3, A4), T *instance) {
 _removeFromList(_identify(instance, member));
 }
private:
#pragma mark - Address conversion
 //
 // @brief Unions a generic lvalue object address and an integral identifier.
 //
 template<typename T>
 union AddressCast {
 explicit AddressCast(T _type) : type(_type) { }
 T type;
 Identifier address;
 };
 //
 // @brief Returns a unique identifier for a given member function pointer and instance pointer.
 //
 template<class C, class M>
 inline static Identifier _identify(C _class, M _member) {
 return AddressCast<C>(_class).address * 10 + AddressCast<M>(_member).address;
 }
#pragma mark - Private inline functions
 //
 // @brief Validates prepared function object and adds it to the container.
 // Contract: handlers with matching identifiers present in the container are silently ignored.
 //
 inline void _addToList(Address a, Handler h, EventFlag flag) {
 // disallow non-unique handlers
 if (flag == EventFlag::ONLY_UNIQUE && hasSubscriber(a)) return;
 subscribers.insert(std::make_pair(a, h));
 }
 //
 // @brief Removes bound function object from the container by its address.
 // Contract: non-existent member identifier is silently ignored.
 //
 inline void _removeFromList(Address a) {
 auto it = subscribers.find(a);
 if (it != subscribers.end())
 subscribers.erase(it);
 else {
 // TODO handle
 }
 }
 //
 // @brief Pairs bound subscribers with their unique memory address.
 // Container has to be unordered for the handlers to be called sequentially.
 //
 std::unordered_map<Identifier, Handler> subscribers;
};
#endif

Here is a simple usage scenario:

struct WidgetEventArgs {
 explicit WidgetEventArgs(std::string _reversedString) : reversedString(_reversedString) { }
 std::string reversedString;
};
class Widget {
public:
 void reverseString(std::string s) {
 auto reversedS = std::string(s.rbegin(), s.rend());
 // fire stringReversed event
 // method 1:
 stringReversed.fire(WidgetEventArgs(reversedS));
 // method 2:
 stringReversed(WidgetEventArgs(reversedS));
 }
 Event<WidgetEventArgs> stringReversed;
};
class Consumer {
public:
 Consumer() {
 // event subscription to a member function
 _widget.stringReversed.bind(&Consumer::_onStringReversed1, this);
 _widget.stringReversed.bind(&Consumer::_onStringReversed2, this);
 // ignore subscription if handler function already subscribed:
 _widget.stringReversed.bind(&Consumer::_onStringReversed2, this, EventFlag::ONLY_UNIQUE);
 // unsubscribe event
 _widget.stringReversed.unbind(&Consumer::_onStringReversed2, this);
 // subscribe a lambda
 _widget.stringReversed += [](auto e) {
 std::cout << e.reversedString << " from lambda\n";
 };
 _widget.reverseString("Hello");
 }
private:
 Widget _widget;
 void _onStringReversed1(WidgetEventArgs e) {
 std::cout << e.reversedString << 1 << std::endl;
 }
 void _onStringReversed2(WidgetEventArgs e) {
 std::cout << e.reversedString << 2 << std::endl;
 }
};
int main() {
 Consumer c;
 return 0;
}
asked Feb 2, 2016 at 10:53
\$\endgroup\$
5
  • \$\begingroup\$ @200_success Was it intentional that you removed the C++14 but left the C++11 tag? The question text mentions C++14. \$\endgroup\$ Commented Feb 2, 2016 at 20:43
  • \$\begingroup\$ @5gon12eder There wasn't much point in having a question tagged as both c++11 and c++14. I gave priority to c++11, assuming that that was the minimum version being targeted. \$\endgroup\$ Commented Feb 2, 2016 at 20:45
  • \$\begingroup\$ Since the code uses std::bind, I'd rather like to know if I can suggest C++14 features in my review. \$\endgroup\$ Commented Feb 2, 2016 at 20:47
  • \$\begingroup\$ I'm not an expert for Unions, but doesn't your address cast invoke undefined behavior? \$\endgroup\$ Commented Feb 2, 2016 at 22:14
  • 1
    \$\begingroup\$ To clarify, this code targets C++14. @MikeMb, that's exactly why I am asking for a second opinion. That's the only way I found possible to get address of a class function pointer. \$\endgroup\$ Commented Feb 3, 2016 at 8:51

1 Answer 1

4
\$\begingroup\$

Reserved Identifiers

Unless you are very sure of the rules about what combinations of leading underscore are reserved for the implementation I would suggest that you do not use underscore (_) as a prefix. Please see this excellent answer for what identifiers are reserved.

In particular underscore followed by another underscore or a capital letter is reserved for use by the implementation. For example _MAX_EVENT_ARGS is in violation of this rule.

Infinite recursion

You system is susceptible to infinite recursion (and crash by stack overflow). Although any small example I can construct will feel contrived and pathological, when using an event system in a complex UI these kind of problems do crop up.

Consider the following (contrived) example:

class CurrencyConverter{
public:
 Event<int> euroChanged;
 Event<int> dollarChanged;
 int euro;
 int dollar;
 void onEuroChanged(int v){
 dollar = convert2dollar(v);
 dollarChanged(dollar);
 }
 void onDollarChanged(int v){
 euro = convert2euro(v);
 euroChanged(dollar);
 }
}

You could argue that this is invalid use of the system in the same way as void f(){f();} is an improper use of recursion. The difference here is that in an event system this kind of recursive behaviour is not always as apparent as it is here, and it may involve a multiply deep call chain from several different objects.

I believe that you should implement some kind of checking to make sure that recursion into an already executing event handler is caught and reported early. Or possibly just stopped and turned into a no-op. In the above example if the recursion into the current caller just immediately returned the behaviour would be correct.

Dangling pointers

Another problem with the system is that it is susceptible to dangling pointers. Consider the following (again contrived) example:

Event<X> x;
{
 MyClass m;
 x.bind(&MyClass::onX, &m);
}
x.fire(...); // Ka-blamo!

Now of course the fault here is that you forgot to call unbind but explicit managing of bind/unbind takes us back to the age of raw new/delete with all the associated problems. I really believe that you need to RAII-fy this system to avoid this kind of bugs.

One way to do this is to make bind to take std::shared_ptr<T> instead of T* and then change subscribers to store <std::weak_ptr<T>, Handler>. Of course this has the drawback that you cannot bind to anything that isn't a shared pointer and this may be unacceptable.

Another approach is to use something similar to std::lock_guard but instead have bind return a EventGuard like so: EventGuard bind(...) that will unbind when it destructs. This is not foolproof either but it shows a few ideas for how you could approach this.

Collisions in the Identity

The function to calculate an unique identifier is flawed and is riddled with collisions:

inline static Identifier _identify(C _class, M _member) {
 return AddressCast<C>(_class).address * 10 + AddressCast<M>(_member).address;
}

I read the above as: i = a * 10 + m. Lets call the lowest address that the system can assign to user heap or stack for a_min, conversely call the largest address a_max. The same goes for m with m_min and m_max. The only way the above expression can be safe is if a_min*10 > m_max and a_min*10 > a_max. I fired up my debugger and grabed the first heap pointer I could find: a_min <= {a=0x5a08c00} and the first instruction pointer {m=0x141533749} <= m_max.

Note that a*10 = 0x38457800 < m=0x141533749 --> a_min*10 !> m_max which means that you have collisions in your identity function. This is asking for trouble...

Closing remarks

I think that you can get rid of your template overloads by clever use of a better identification scheme and using variadic template arguments. However I will not go into detail on this due to lack of time.

answered Mar 24, 2016 at 10:33
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.