0

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.

Danny S
  • 457
  • 4
  • 15
  • There is an infinite number of possible events and an infinite number of possible listeners. Every listener is valid for some potential event. If you want to validate against a fixed closed list of events, you need to explicitly build that list somewhere. – n. m. could be an AI Aug 04 '21 at 07:13
  • 1
    Aside: For usability, you will probably want to have `Sub`'s args be `std::decay_t`'d versions of `listener`'s args – Caleth Aug 04 '21 at 08:47
  • 1
    You probably need some mapping between `EventKind` and expected `std::function`. – Jarod42 Aug 04 '21 at 09:27

2 Answers2

2

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
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • This is definitely the best way to go - it's a pity I can't have a custom error message when it's compiling, but trapping these at compile time is much better than at run time. Some fiddliness was required for me to create the appropriate `Sub` but I did manage it using something similar to https://stackoverflow.com/a/15418928/653093 to pull the args from the std::function out. – Danny S Aug 05 '21 at 05:41
0

Here's a solution I managed to get working - it avoids the Event class hierarchy but seems to work OK.

// Validation of event listener args (general case)
template<typename... Args>
inline bool validate_event_listener_args(EventKind)
{ return false; }

// Validation of event listener args (no args case)
template<>
inline bool validate_event_listener_args<>(EventKind kind)
{
    return (kind == EventKind::second_kind || ... );
}

// Validation of event listener args (for FirstEvent)
template<>
inline bool validate_event_listener_args<Foo, Bar, Qux>(EventKind kind)
{
    return (kind == EventKind::first_kind);
}

// ...

I can then, in add_listener, call validate_event_listener_args<Args...>(event.kind()) and throw an exception if it returns false. This is useful for runtime validation.

Danny S
  • 457
  • 4
  • 15