3
\$\begingroup\$

Problem

I would like to make an efficient, extensible, and potentially asynchronous parallelizable event system using modern features in c++20.

My solution

I've constructed a templated event struct by category and group:


#include <algorithm>
#include <execution>
#include <functional>
#include <vector>

namespace System::Event {
enum class Type {
    Group = 0,
    Tick, Update, Render, Terminate,
    Connect, Disconnect,
    Move, Close, Resize, Refresh, Focus, Defocus, Maximize, Minimize, Restore,
    Press, Release, Type,
    Scroll,
};

enum class Group {
    Undefined   = 0,
    Instance    = Numerics::Bit(0), // 1
    Monitor     = Numerics::Bit(1), // 2
    Window      = Numerics::Bit(2), // 4
    Input       = Numerics::Bit(3),
    Keyboard    = Numerics::Bit(4),
    Mouse       = Numerics::Bit(5),
    Controller  = Numerics::Bit(6),
    Button      = Numerics::Bit(7),
};
Structure::Concept::Tag bitmask(System::Event::Group);
}

Where the group acts as a type safe bitmask with overloaded operators. (Usage below)

The base event class only contains a status member variable (used, unused, lost, etc).


namespace System::Event {
template<Type T, Group G>
struct Event {
    System::Structure::Status::Event status;
};
}

There are various implementations of events, but here is a simple Window Close Event:


namespace System::Event::Window {
    struct Close : public Event <Type::Close, Group::Instance + Group::Window> {
       Close() : Event({System::Structure::Status::Event::Unused}) {}
       class Manager;
    };
}

Now, I want a method to dispatch a Window::Close event.

namespace System::Event::Window {
    class Close::Manager {
        private:
            using Callback = Structure::Callback<System::Event::Window::Close &>; // std::function<>
        public:
            void listen(Callback callback) {
                m_Callbacks.push_back(callback);
            }
            void deafen(Callback callback) {
                m_Callbacks.erase(std::remove_if(std::execution::par_unseq, std::begin(m_Callbacks), std::end(m_Callbacks), [&callback](const Callback & other) {
                    return callback.target<Callback>() == other.target<Callback>();
                }));
            }
            void queue(Window::Close & event) {
                m_Event = event;
            }
            void latest() {
                std::for_each(std::begin(m_Callbacks), std::end(m_Callbacks), [&](const Callback & callback) {
                    callback(m_Event);
                });
            }
         private:
            std::vector<Callback> m_Callbacks;
            Window::Close m_Event;
    };
}

Usage

This is an example unit test. (Not final, demonstrates usage)


    auto callback = [](System::Event::Window::Close & event) {
        EXPECT_TRUE(event.status == System::Structure::Status::Event::Unused);
        event.status = System::Structure::Status::Event::Used;
    };
    System::Event::Window::Close::Manager manager;
    manager.listen(callback);
    manager.latest();
    System::Event::Window::Close event;
    manager.queue(event);
    manager.deafen(callback);
    manager.latest();
    manager.listen(callback);
    manager.latest();

Live Example

Comments/Questions

  1. This method will produce a large amount of redundant code, I'd like to make the Manager class a template specialization at some point.
  2. This will require each type of event to have a manager. How can I consolidate all Window events into one manager without impacting performance or using runtime polymorphism?
  3. I will migrate to use std::ranges after clang supports it.
  4. At some point, I'd like to additionally include execution policies in the dispatch methods, I have yet to determine the best choices.
  5. queue will eventually involve a stack of stored events so that I can determine how to interpolate which events matter if the system is lagging or otherwise. For example if there is a Mouse move from (1,1) to (180,180), with a (1,1) delta, it'd be scaled to frame-rates instead of rendering every delta.
  6. I understand this is not the intended use of namespace or class namespaces, it is a personal preference.
  7. This is not the best design, I'm exploring, please be critical of all flaws!
  8. Owning repository, for the curious. I am continuing to change the design since this post, however.

Notes

I am very intentionally avoiding run time polymorphism. Thank you for reviewing!

\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.