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;
}
\$\endgroup\$
  • \$\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\$ – 5gon12eder Feb 2 '16 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\$ – 200_success Feb 2 '16 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\$ – 5gon12eder Feb 2 '16 at 20:47
  • \$\begingroup\$ I'm not an expert for Unions, but doesn't your address cast invoke undefined behavior? \$\endgroup\$ – MikeMB Feb 2 '16 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\$ – Crossfire Feb 3 '16 at 8:51
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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.