8
\$\begingroup\$

Introduction


I have just started learning templates and experimenting with function pointers. I wanted to create an event system that met a couple of goals:

  • Event types are PODs, and do not inherit from a base 'Event' class
  • External API is simple, no binding or passing in lambdas etc.
  • No explicit registering of event types by user, any POD struct like object should work

The implementation currently works, however hasn't been heavily tested. The code will be used in my small "game engine" project, done purely as a learning exercise.

Focus


I am most interested in the way I am storing and calling the functions. I wanted a way to store any arbitrary event type that met the conditions described above.

I'm not convinced on the way I am handling the ListenerHandles which are solely used to enable the remove_listener feature. However this is of less concern to me than my previous point.

Future


  • Multithreading/Async
  • Delayed event queues

Usage


I'll start with what it looks like to use the API, I am relatively happy with this aspect:

#include "EventManager.h"

const int key_w = 87;

struct CollisionEvent
{
    int x;
    int y;
};

struct KeyPressEvent
{
    int key_code;
};


class InputHandler
{
public:
    InputHandler()
    {
        EventManager::listen<KeyPressEvent>(this, &InputHandler::on_key_press);
    }
private:
    void on_key_press(KeyPressEvent* event)
    {
        // ...
    }
};


void handle_collision(CollisionEvent* event)
{
    // ...
}


int main()
{
    InputHandler input_handler;
    EventManager::fire<KeyPressEvent>(key_w);


    ListenerHandle handle = EventManager::listen<CollisionEvent>(&handle_collision);
    EventManager::fire<CollisionEvent>(10, 3);
    EventManager::remove_listener(handle);
}

Implementation


It is implemented in a single header EventManager.h, and is roughly ~140 lines without whitespace. I will divide each part into its own block for readability.

ListenerHandle


A ListenerHandle is returned to the user upon registering an event callback. Each callback function is uniquely associated with one handle, allowing users to remove their callback.

#pragma once

#include <cassert>
#include <cstdint>
#include <vector>
#include <memory>
#include <functional>
#include <queue>


typedef std::uint32_t EventID;

class ListenerHandle
{
public:
    ListenerHandle(std::uint32_t sparse_index = 0u, std::uint32_t dense_index = 0u, std::uint32_t version = 0u, EventID event_id = 0u)
        : m_sparse_index(sparse_index),
          m_dense_index(dense_index),
          m_version(version),
          m_event_id(event_id) {}


public:
    std::uint32_t m_sparse_index;
    std::uint32_t m_dense_index;
    std::uint32_t m_version;
    EventID       m_event_id;
    

    friend class EventManager;

    template<typename T>
    friend struct CallbackContainer;
};

CallbackContainer


A callback container is used to store each callback of the same event type (i.e from the example above, all KeyPressEvent callbacks would be stored together in an instance of CallbackContainer).

A sparse set is used to track which ListenerHandle belongs to which callback. Each index in the sparse-set is versioned, so you can only remove the callback originally associated with your handle.

struct CallbackContainerBase
{
    virtual void remove_callback(const ListenerHandle& handle) = 0;
};
    
template<typename T>
struct CallbackContainer : CallbackContainerBase
{
    CallbackContainer(EventID event_id) : event_id{ event_id } {}

    std::vector<std::function<void(T*)>>    callbacks;
    std::vector<ListenerHandle>             handles;
    EventID                                 event_id;

    std::vector<ListenerHandle>             sparse;
    std::vector<std::uint32_t>              dense;
    std::queue<std::uint32_t>               free_sparse_indices;

    template<typename T_Function>
    auto add_callback(T_Function callback)->ListenerHandle;
    void remove_callback(const ListenerHandle& handle) override;
};


template<typename T>
inline void CallbackContainer<T>::remove_callback(const ListenerHandle& handle)
{
    assert(handle.m_version == sparse[handle.m_sparse_index].m_version);

    sparse[handle.m_sparse_index].m_version++;

    std::uint32_t remove_index = sparse[handle.m_sparse_index].m_dense_index;
    std::uint32_t last_index = callbacks.size() - 1;

    dense[remove_index] = dense[last_index];
    sparse[dense[remove_index]].m_dense_index = remove_index;

    std::swap(callbacks[remove_index], callbacks[last_index]);

    free_sparse_indices.push(handle.m_sparse_index);
    callbacks.pop_back();
    dense.pop_back();
}

template<typename T>
template<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
    std::uint32_t sparse_index;

    if (free_sparse_indices.empty())
    {
        sparse_index = callbacks.size();
        sparse.emplace_back(sparse_index, sparse_index, 0u, event_id);
    }
    else
    {
        sparse_index = free_sparse_indices.front();
        free_sparse_indices.pop();
    }

    dense.push_back(sparse_index);
    callbacks.emplace_back(callback);

    return sparse[sparse_index];
}

EventManager


This is the class that users interact with, a trivial system is used to create run-time available IDs to identify event types, which act as indexes into the vector of CallbackContainer's.

Each CallbackContainer is stored as its base CallbackContainerBase to allow for the storage of any arbitrary type, and then casted to the correct type when needed.


class EventManager
{
    using CallbackContainers = std::vector<std::unique_ptr<CallbackContainerBase>>;
public:
    template<typename T, typename T_Function>
    static auto listen(T_Function callback)->ListenerHandle;

    template<typename T, typename T_Instance, typename T_Function>
    static auto listen(T_Instance* instance, T_Function callback)->ListenerHandle;

    static void remove_listener(const ListenerHandle& handle);

    template<typename T, typename... T_Args>
    static void fire(T_Args...args);


private:
    template<typename T>
    static auto get_event_id()->EventID;

    template<typename T>
    static auto register_event()->EventID;

private:
    static inline CallbackContainers    s_callbacks;
    static inline EventID               s_next_event_id{ 0u };
};


template<typename T, typename T_Function>
inline ListenerHandle EventManager::listen(T_Function callback)
{
    return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback(callback);
}

template<typename T, typename T_Instance, typename T_Function>
inline ListenerHandle EventManager::listen(T_Instance* instance, T_Function callback)
{
    return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback([instance, callback](T* event) { (instance->*callback)(event); });
}

inline void EventManager::remove_listener(const ListenerHandle& handle)
{
    s_callbacks[handle.m_event_id]->remove_callback(handle);
}

template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args ...args)
{
    T event{ args... };

    auto& callbacks = static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->callbacks;
    for (auto& callback : callbacks)
        callback(&event);
}

template<typename T>
inline EventID EventManager::get_event_id()
{
    static EventID event_id = register_event<T>();
    return event_id;
}

template<typename T>
inline EventID EventManager::register_event()
{
    s_callbacks.emplace_back(std::make_unique<CallbackContainer<T*>>(s_next_event_id));
    return s_next_event_id++;
}

Thank you for your time, it is greatly appreciated!

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

No need for s_callbacks[]

You have a vector of pointers to CallbackContainerBase. But to get the index into that, you have get_event_id(), which creates a unique ID once, and stores it in a static local variable event_id. But you can do exactly the same, not with an EventID, but with a CallbackContainer. Consider:

template<typename T>
inline CallbackContainer<T>& EventManager::get_callbacks()
{
    static CallbackContainer<T> callbacks{}; // no need for an ID anymore
    return callbacks;
}

But you can also use inline variables, which you are already using, in combination with templates, so you can write:

class EventManager
{
    template<typename T>
    static inline CallbackContainer<T> s_callbacks;
    …
};

This greatly simplifies the rest of your code; for example:

template<typename T, typename T_Function>
inline ListenerHandle EventManager::listen(T_Function callback)
{
    return s_callbacks<T>.add_callback(callback);
}

Simplify the callback container

Your CallbackContainer class is quite complicated. It has five containers. How many callbacks do you think you need to store before the overhead of this is worth it? Consider that the size of an empty std::function<> is typically 32 bytes on 64-bit machines, and your ListernerHandle itself is already 16 bytes.

The individual indices in a handle are only 32 bits. Ideally the code registering a listener only needs a handle that is 32 bits to uniquely identify the listener.

Here is another approach that is simpler, can still handle lots of callbacks, has \$O(1)\$ insertion and removal, but is not sparse:

typename<typename T>
struct CallbackContainer
{
    struct ListenerHandle
    {
        std::uint32_t id;
    };

    std::vector<std::function<void(T&)> callbacks;
    std::vector<ListenerHandle> free_handles;

    template<typename T_Function>
    auto add_callback(T_Function callback) -> ListenerHandle;
    void remove_callback(ListenerHandle handle) override;
};

template<typename T>
template<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
    ListenerHandle handle;

    if (free_handles.empty())
    {
        handle = callbacks.size();
        callbacks.emplace_back(handle);
    }
    else
    {
        handle = free_handles.back();
        free_handles.pop_back();
        callbacks[handle] = callback;
    }

    return handle;
}

template<typename T>
void CallbackContainer<T>::remove_callback(ListenerHandle handle)
{
    callbacks[handle] = nullptr;
    free_handles.push_back(handle);
}

It just requires a slight modification when firing events:

template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args ...args)
{
    T event{ args... };

    for (auto& callback : s_callbacks<T>.callbacks)
        if (callback)
            callback(event);
}

Also note that I moved the declaration of ListenerHandle into CallbackContainer. This makes ListernerHandle implicitly templated on T as well, adding more type safety. It does require that you pass the template type to EventManager::remove_listener though:

template<typename T>
inline void EventManager::remove_listener(CallbackContainer<T>::ListenerHandle handle)
{
    s_callbacks<T>.remove_callback(handle);
}

But where you are calling remove_listener() you probably don't need to make any changes, as T can be deduced from the handler passed as the parameter.

There are other tradeoffs that can be made; you can keep callbacks dense and still avoid most of the other containers, but then adding and/or removing might not be possible in \$O(1)\$ time anymore. For example:

typename<typename T>
struct CallbackContainer
{
    …
    std::vector<std::function<void(T&)> callbacks;
    std::vector<ListenerHandle> handles;
    …
};

typename<typename T>
typename<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
    static ListenerHandle next_handle;

    do {
        ++next_handle.id;
    } while (std::find(handles.begin(), handles.end(), next_handle) != handles.end());

    callbacks.push_back(callback);
    handles.push_back(next_handle);

    return next_handle;
}

template<typename T>
void CallbackContainer<T>::remove_callback(ListenerHandle handle)
{
    auto pos = std::find(handles.begin(), handles.end(), handle) - handles.begin();
    std::swap(handles[pos], handles.back());
    handles.pop_back();
    std::swap(callbacks.pos[], callbacks.back());
    callbacks.pop_back();
}

Creating the event object

When you call fire(), you construct a T event first, using the arguments passed to fire(). However, you take all arguments by value, but it is usually better to use perfect forwarding, like so:

template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args&& ...args)
{
    const T event(std::forward<T_Args>(args)...);
    …
}

This allows proper forwarding of references and also allows for move-construction of event. Note that I also made it const here, as you might not want one callback to modify the event data when another callback still needs to be fired.

Note that in addition to that version of fire(), you can add an additional overload that takes a const reference to a T:

template<typename T>
inline void EventManager::fire(const T& event)
{
    …
}

This overload will allow T to be deduced when you pass it an existing event object:

CollisionEvent collision_event(10, 3);
EventManager::fire(collision_event);

Avoid unnecessary use of friend

Using friend is a code smell; there is a good chance that code using it has too much coupling between different classes. Sometimes it is necessary, but in other cases it is unnecessary and can be avoided.

I don't see why ListenerHandle has to be a friend of EventManager and CallbackContainer<T>; everything it has is public.

Add a fire() function to CallbackContainer

EventManager has to be able to access CallbackContainer<T>::callbacks in order for fire() to work. At the moment, CallbackContainer is a struct where everything is public. However, ideally you would make this a class, so it can hide its implementation details. You do need to fire events though, so to avoid having to add a friend EventManager, I would add a public fire() member function to CallbackContainer, which EventManager can then call.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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