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);
}

Here 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 runtime polymorphism.

\$\endgroup\$

0

Your Answer

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

Browse other questions tagged or ask your own question.