Search code examples
c++parametersdom-eventsc++17std-function

std::function: cannot convert derived class parameter to base class parameter


I'm attempting to mimic JavaScript style Event/EventTarget operations in C++ (C++17). I have an Event base class that works as needed, and a KeyboardEvent class that inherits from Event. My EventTarget.addEventListener function stores EventObjects in a vector that can be called with dispatchEvent.

I am able to use addEventListener to store callback functions using a Event parameter, e.g. onGenericEvent(Event event). However, I am unable to use addEventListener to add callback functions that take a KeyboardEvent parameter, e.g. onKeyDown(KeyboardEvent event).

I was expecting std::function<void(Event)> to accept KeyboardEvent since it inherits from Event but I receive a compilation error:

ERROR: cannot convert 'void (*)(KeyboardEvent)' to 'std::function<void(Event)>'

How should I proceed?

#include <iostream>
#include <functional>
#include <string>
#include <vector>


class Event {
public:
    Event(std::string _type, void* _pointer) {
        type = _type;
        pointer = _pointer;
    }
    std::string type;
    void* pointer;
};


struct EventObject {
    std::string type;
    std::function<void(Event)> listener;
    void* pointer;
};

class EventTarget {
public:
    static void addEventListener(std::string type, std::function<void(Event)> listener, void* pointer){
        EventObject eventObj;
        eventObj.type = type;
        eventObj.listener = listener;
        eventObj.pointer = pointer;
        events.push_back(eventObj);
    }
    static void dispatchEvent(Event event){
        for (unsigned int n = 0; n < events.size(); ++n) {
            if (event.type == events[n].type && event.pointer == events[n].pointer) {
                events[n].listener(event);
            }
        }
    }
    static std::vector<EventObject> events;
};
std::vector<EventObject> EventTarget::events;


class KeyboardEvent : public Event {
public:
    KeyboardEvent(std::string _type, void* _pointer) : Event(_type, _pointer) {
        key = "random key";
    }
    std::string key;
};


class App
{
public:
    static void onGenericEvent(Event event) {
        printf("event.type = %s \n", event.type.c_str());
    }
    
    static void onKeyDown(KeyboardEvent event) {
        printf("event.type = %s \n", event.type.c_str());  // print Event class property
        printf("event.key = %s \n", event.key.c_str());    // print KeyboardEvent class property
    }
};


int main() {
    App* app = new App();
    
    EventTarget::addEventListener("generic", &App::onGenericEvent, app);
    Event event("generic", app);
    EventTarget::dispatchEvent(event); // simulate generic event
    
    EventTarget::addEventListener("keydown", &App::onKeyDown, app);  //ERROR: cannot convert 'void (*)(KeyboardEvent)' to std::function<void(Event)>'
    KeyboardEvent keyEvent("keydown", app);                         
    EventTarget::dispatchEvent(keyEvent);  // simulate keydown event
}

Online test code: https://onlinegdb.com/uJBz6g4lr


Solution

  • Note: First and foremost you are going to slice your Event objects if you pass them around by value like that. You need to pass references or pointers for polymorphism to work!


    void(KeyboardEvent&) is more specific than void(Event&), so std::function<void(Event&)> cannot wrap a void(KeyboardEvent&).

    Consider if it could, then this would be possible:

    void handler(KeybaordEvent& evt)
    {
        std::cout << evt.key;
    }
    
    int main()
    {
        std::function<void(Event&)> callback(&handler);
        MouseEvent mouseEvent;  // Imagine MouseEvent also derives from Event
        callback(mouseEvent);
    }
    

    Since a MouseEvent is an Event a std::function<void(Event&)> can accept a MouseEvent parameter. If it was allowed to wrap a void(KeyboardEvent&) that would result in the handler being passed the wrong type. For that reason, what you want is not allowed.

    Remember, type checking is done at compile time. Just because you (try to) never pass the wrong type by checking the EventObject's type member at runtime doesn't mean you can have mismatched types at compile time.


    There are two ways to deal with this:

    1. Have all of your handlers accept an Event and do a runtime check that they got the type of event that they were expecting (i.e. using dynamic_cast).
    2. Use templates to ensure at compile time that you always pass the correct type of event to the correct handler.

    As an example of the second option, you could so something like this:

    template <typename EventType>
    struct EventObject {
        std::string type;
        std::function<void(EventType)> listener;
        void* pointer;
    };
    
    class EventTarget {
    public:
        template <typename EventType>
        static void addEventListener(std::string type, std::function<void(EventType)> listener, void* pointer){
            EventObject<EventType> eventObj;
            eventObj.type = type;
            eventObj.listener = listener;
            eventObj.pointer = pointer;
            events<EventType>.push_back(eventObj);
        }
        
        template <typename EventType>
        static void dispatchEvent(EventType event){
            for (auto& eventObj : events<EventType>) {
                eventObj.listener(event);
            }
        }
        
        template <typename EventType>
        static std::vector<EventObject<EventType>> events;
    };
    
    template <typename EventType>
    std::vector<EventObject<EventType>> EventTarget::events;
    
    
    class KeyboardEvent : public Event {
    public:
        KeyboardEvent(std::string _type, void* _pointer) : Event(_type, _pointer) {
            key = "random key";
        }
        std::string key;
    };
    
    
    class App
    {
    public:
        static void onGenericEvent(Event event) {
            printf("event.type = %s \n", event.type.c_str());
        }
        
        static void onKeyDown(KeyboardEvent event) {
            printf("event.type = %s \n", event.type.c_str());  // print Event class property
            printf("event.key = %s \n", event.key.c_str());    // print KeyboardEvent class property
        }
    };
    
    
    int main() {
        App* app = new App();
        
        EventTarget::addEventListener<Event>("generic", &App::onGenericEvent, app);
        Event event("generic", app);
        EventTarget::dispatchEvent(event); // simulate generic event
        
        EventTarget::addEventListener<KeyboardEvent>("keydown", &App::onKeyDown, app);  //ERROR: cannot convert 'void (*)(KeyboardEvent)' to std::function<void(Event)>'
        KeyboardEvent keyEvent("keydown", app);                         
        EventTarget::dispatchEvent(keyEvent);  // simulate keydown event
    }
    

    Demo

    This creates separate events vectors for each type of event. Now the compiler can check at compile time that the right type of event will always be passed to each handler.