Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Event handling 3.0 (with steroids) #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: master
Choose a base branch
from

Conversation

Vraiment
Copy link
Contributor

@Vraiment Vraiment commented Aug 9, 2017

So after a lot of work I managed to finish the implementation of an idea I had when I started working on this event handler logic. This is basically a generalization of the PollEvent() and PollAllEvents() functions I had previously published.

How does it work?

Definitions

First let us define what an event handler is, it can come in two flavors:

  • Event handler functor: It's an object that can be used as object(event) where event is of any valid event type. This event handler definition includes free functions, lambdas and classes with operator() overloaded.
  • Event handler object: An event handler object is an object that can be used as object.HandleEvent(event), no inheritance is needed. A single object can handle more than one event type.

Both flavors must return void.

The valid event types are defined by the data fields of SDL_Event rather than the SDL_EventType enum. The actual SDL_Type is also considered a valid event type and if an event handler accepts this type then all the events will be passed to it.

Usage

The user only has to define a list of event handlers and pass it to one of the two event polling functions defined in EventPolling.hh, ex:

    struct PlayerEvents {
        void HandleEvent(SDL_KeyboardEvent);
        void HandleEvent(SDL_MouseMotionEvent);
    };

    void LogEvents(SDL_Event);

    int main(...) {
        // ...

        auto quit = false;
        auto quitEventHandler = [&quit](SDL_QuitEvent) { quit = true; }

        PlayerEvents userEvents = // ... get user events 

        // Game loop
        while (!quit) {
            PollAllEvents(quitEventHandler, userEvents, LogEvents);

            // Game logic...
        }
    }

In the previous example user defines three event handlers:

  1. A custom class that accepts keyboard events and mouse motion events (no inheritance involved)
  2. A free function that accepts any kind of event
  3. A lambda that accepts quit events

This was my objective, the usage is extremely simple!

The guts

This makes heavy use of template metaprogramming, so the language might be a little odd. I will be describing the logic in a bottom-up fashion:

  1. Three utility metaprogramming operations (these can be found in the Private/Utility.hh file):

    1. Or<...> Or operation for template metaprogramming, expands to std::true_type if any of the values passed to the template are true, otherwise expands to std::false_type.
    2. And<...> And operation for template metaprogramming, expands to std::true_type if all of the values passed to the template are true, otherwise expands to std::false_type.
    3. TupleHasType<T, std::tuple<...>> Expands to std::true_type if T is a value defined in the given tuple, otherwise expands to std::false_type.
  2. ValidEventTypes is defined as a tuple of valid event types in Private/EventHandler.hh

  3. Three different "template functions" to identify if a type can handle an event (also defined in Private/EventHandler.hh):

    1. IsEventHandlerFunctor<EventHandlerType, EventType> expands to std::true_type if EventHandlerType adheres to the definition of an event handler functor described above of the given EventType, otherwise expands to std::false_type. The same is true for any event handler object with IsEventHandlerObject<EventHandlerType, EventType>.
    2. IsEventHandler<EventHandlerType, EventType> expands to the equivalent of (using the previous definitions): (IsEventHandlerFunctor<EventHandlerType, EventType> OR IsEventHandlerObject<EventHandlerType, EventType>) AND TupleHasType<EventType, ValidEventTypes>
  4. EventTypeFilter is defined (in Private/EventTypeFilters.hh) to map an instance of SDL_Event to any of the types defined in ValidEventTypes. Basically this "template function" can detect if a given instance of SDL_Event is of a given EventType and retrieve the EventType value from it. This must be defined for all the types present in ValidEventTypes.

  5. The function DispatchEventHandlerFunctor(const EventType&, EventHandlerType&&) has two definitions (in Private/EventDispatching.hh), one to apply the given EventType to the given EventHandlerType in case the later is an event handler functor for EventType and other that is no-op. The same is true for any event handler object with DispatchEventHandlerObject(const EventType&, EventHandlerType&&).

  6. The EventDispatcher<ValidEventHandler, EventHandlerType, EventTypes...> "template function" is a class defined (in EventDispatching.hh) to dispatch the given SDL_Event to the given instance of EventHandlerType (using both DispatchEventHandlerFunctor and DispatchEventHandlerObject) for each event type in EventTypes. ValidEventHandler is a helper value to identify if an valid/invalid event handler has been passed (in other words if IsEventHandler<EventHandlerType, EventType>), the final expansion of EventDispatcher will do a static assert on that value to signal an error if an invalid event handler was passed.

  7. The function DispatchEvent(const SDL_Event &, EventHandlerTypes&&...) is defined to call the EventDispatcher "template function" with each of the given event handlers and each of the event types defined in ValidEventTypes.

  8. Finally PollEvent(EventHandlerTypes&&...) use the standard SDL SDL_PollEvent() to retrieve an event and if any event is retrieved it is dispatched to all event handlers using DispatchEvent. PollAllEvents(EventHandlerTypes&&...) makes use of PollEvent(EventHandlerTypes&&...) to do the same until there is no events left to poll. Both defined in EventPolling.hh.

Again, this makes heavy usage of template metaprogramming, but a rough translation to C++-like code would be:

constexpr auto validEventTypes = [SDL_Event,
    SDL_AudioDeviceEvent,
    SDL_ControllerAxisEvent...];

int PollAllEvents(EventHandlerTypes&&... eventHandlers) {
    auto count = 0;
    auto event = SDL_Event{};
    
    while (!SDL_PollEvent(&event)) {
        for (auto eventHandler : eventHandlers) {
            auto isValidEventHandler = false;
            
            for (auto eventType : validEventTypes) {
                // This function is really "Filter::ShouldHandleEvent"
                if (EventIsOfType(event, eventType)) {
                    if constexpr (ValidEventHandlerFunctor(eventHandler, eventType)) {
                        eventHandler(event);
                    }
                    
                    if constexpr (ValidEventHandlerObject(eventHandler, eventType)) {
                        eventHandler.HandleEvent(event);
                    }
                }
                
                // This function is really called "IsEventHandler"
                isValidEventHandler |= ValidEventHandler(eventHandler, eventType);
            }
            
            if (!isValidEventHandler) throw;
        }
    }
    
    return count;
}

Concerns

  1. I don't think the user should have access to the template metaprogramming logic, the only public API they should care about is PollEvent and PollAllEvents. Hence I created a Private folder and placed such code in the SDL2pp::Private namespace, if you think it should be moved/renamed please let me know.
  2. An struct/class can define both operator() and HandleEvent() functions for the same type, I'm not sure what's more intuitive. Rise an error in case both are defined and mention this is ambiguous, execute both or just execute one. Currently the code executes both one after another.
  3. Due the technics I'm using to detect the signature of the event handlers, the argument can be in the form of: EventType, const EventType and const EventType &. If the argument is defined as EventType & then it will be marked as an invalid event handler. If a custom type define both it will silently ignore the invalid one, this is conceptually and technically correct but I think it may cause some headaches:
    struct CustomEventHandler {
        void HandleEvent(SDL_MouseMotionEvent); // Valid event handler
        void HandleEvent(SDL_KeyboardEvent &); // Invalid event handler, will silently ignore it
    };
    
  4. You mentioned the intention to have custom function names (like HandleKeyDown) but this would require an enormous amount of code given the amount of possible events and on top of that how would know what exact event the user wants to handle for functors (ex: void foo(SDL_KeyboardEvent) wants to handle key down, key up or both events?). I think the current approach is a healthy compromise between readability and code behind the scenes.

@Vraiment Vraiment changed the title Event handler 3.0 (with steroids) Event handling 3.0 (with steroids) Aug 9, 2017
@Vraiment Vraiment force-pushed the event_handler_3.0 branch from f6bee51 to 31946f3 Compare August 9, 2017 07:41
@Vraiment Vraiment force-pushed the event_handler_3.0 branch from 31946f3 to 154eed8 Compare August 9, 2017 07:45
@jep-dev
Copy link

jep-dev commented Aug 20, 2017

Considering your comfort with template metaprogramming, have you considered using CRTP instead of / in addition to lambda-based handling? It allows compile-time resolution of the 'most derived' definition of methods and functions, without the overhead of dynamic polymorphism. The resulting syntax would be very similar, except the lambda would be replaced by a method override, and composite handlers could be formed via mixins. The CRTP interfaces are effectively promises of an implementation of specific event handling methods, satisfied by the derived class or at least one of its ancestors, with a compile time error resulting from a missing implementation. (It's easy enough to mix in a default implementation in later derivations.)
My knowledge on the subject comes from personal experimentation which almost certainly falls under the category of 'premature optimization', but as I understand it, std::function makes heavy use of virtualization, and deeper class hierarchies, nested or chained callbacks, etc. would suffer the most. If profiling revealed that a significant amount of time is spent handling events, the CRTP approach could yield serious gains - inlining of entire compositions, taking advantage of compiler optimizations, and more. All of this is possible with C++11.
My apologies if this is unclear or something you've already done (is that what you meant by 'Event handler object'?) - I'm more or less running on empty - but I thought it was worth mentioning in any case. I found a basic page on combining static and dynamic polymorphism here and a more intricate example explaining the role of CRTP in this concept here along with some applications toward the bottom and in the comments.

@Vraiment
Copy link
Contributor Author

Vraiment commented Aug 20, 2017

My main focus was removing the need for inheritance so user can just write their classes without the need to override methods or the like.

About the overhead of std::function I've read comments saying is slow and others saying that is fast. You don't need to pass a std::function to the event polling function but, from what I understand, the type of lambda is not defined in the standard and what most compiler do is just storing lambdas in std::function objects hence if you use lambdas very likely you are using std::function. It sounds like a real concern, but lets have some numbers to back it up.

EDIT: I've been thinking about the overhead std::function may add and there are two things that I should make clear:

  • If you are passing lambda and the compiler converts it to a std::function there is nothing we can do to alleviate this issue. It will happen whatever is the case.
  • My implementation doesn't make use of std::function other than to detect if an object is an event handler functor, at line 87 of EventHandler.hh it verifies at compile time if the given type is convertible to a std::function but it doesn't convert it. This check is to verify if you can use an instance of the given type as myInstance(event);. If the user decides to use std::function or a custom class with the parenthesis operator overloaded or a lambda is up to them.

@Vraiment
Copy link
Contributor Author

bump?

@jep-dev
Copy link

jep-dev commented Sep 14, 2017

Sorry, my work is going well but I'm being extra-cautious. I can easily show the general difference between the performance of calls with std::function and without. For SDL2/SDL2pp, I either need to show that the CRTP approach is more efficient everywhere, or I need to find the 'triple points' for different use cases.

For now, if developers have concern about performance, but need an inheritance model, they can use the non-std::function version as it stands and delegate handling to a CRTP-based implementation.

@Vraiment
Copy link
Contributor Author

@AMDmi3, ping?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants