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;
}
1 Answer 1
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.
Explore related questions
See similar questions with these tags.
std::bind
, I'd rather like to know if I can suggest C++14 features in my review. \$\endgroup\$