Search code examples
c++c++11c++14variadic-functionsparameter-pack

Validating std::function with variadic args against a 'kind' hierarchy


I have a system (C++14, using Visual Studio 2015 & GCC 4.9.2) where we have a number of different kinds of 'events' that can cause a callback to occur, and a hierarchy of classes which identify the kind of event and other custom properties specific to that kind of event.

There is an event manager which can take in a specific event object, and a listener function to be called when the event occurs (subscriptions).

Some events will pass certain arguments to the listener callback, but the types of the arguments depends on the event kind.

Is there a way I can validate that the listener args passed in (variadic) are valid for the event object?

enum class EventKind {
   first_kind, second_kind, third_kind
};

class EventBase {
public:
    virtual EventKind kind() const = 0; // each subclass returns its EventKind identifier
    virtual bool matches(const EventBase&) const = 0;
};

class FirstEvent : public EventBase {
public:
    EventKind kind() const final { return EventKind::first_kind; }
    bool matches(const EventBase&) const final; // compare kind and other properties

    // other properties unique to FirstEvent
};

// other event subclasses ...

class EventManager {
public:
    template<typename... Args>
    void add_listener(const EventBase& event, std::function<void(Args...)> listener) {
        // validation of Args... based on event.kind() goes here...
    }
    
    template<typename... Args>
    trigger(const EventBase& event, Args... args) {
       // called when event occurs
       // internal lookup in the manager is done to find any listeners connected
       // with this event object, and then we call it...
       for (auto s : subscriptions[event.kind()]) {
          if (event.matches(*(s->event))) {
              auto* ss = dynamic_cast<Sub<Args...> *>(s);
              if (ss && ss->listener) { ss->listener(args...); }
          }
       }
    }

private:
    struct SubBase {
        EventBase* event;
    };
    template<typename... Args>
    struct Sub : public SubBase {
        std::function<void(Args...)> listener;
    };

    std::map<EventKind, std::vector<SubBase *>> subscriptions;
};

I already have working code in place which can store the listener function for later callback (similar to the above), but the matching of the Args... parameter pack/variadic arguments is only done at the time the event is triggered in the event manager - and so, of course, if the original listener has a mismatching set of arguments, it will not be called (and there's silent failure).

Being able to validate this list of arguments when the listener is added, based on the event type (hopefully using the class hierarchy somehow) would be great. Anyone have ideas?

Note: I'm currently limited to C++14 / Visual Studio 2014 / gcc 4.9.2 so can't use any C++17 constructs.


Solution

  • Require the concrete event type at registration time, and have the event provide the listener type.

    class FirstEvent : public EventBase {
    public:
        EventKind kind() const final { return EventKind::first_kind; }
        bool matches(const EventBase&) const final; // compare kind and other properties
    
        using listener_type = std::function<void(Foo, Bar, Baz)>;
    };
    
    template<typename Event>
    using listener_t = typename Event::listener_type;
    
    template<typename Event>
    void EventManager::add_listener(listener_t<Event> listener) {
        // can only be called with something that accepts the correct args
    }