No, my window-class is still a window class. It just derives an on-event-function from an event-class, so the caller can call it. Literally like your example.
Just be careful about how you think about this problem. Inheritance should be translated to English as "is a". As in, if Window inherits from EventFoo, then "Window is an EventFoo."
This is more than just taxonomy, too. Remember that inheritance implies implicit up-casting and potential "object slicing" problems.
That's not to say that inheritance is wrong in this case specifically, but it should be thought through carefully.
For example, make very sure you're thinking through how to disable object slicing, e.g. make sure your EventEmitter has protected copy/move operations and destructor.
template <typename Event>
class EventEmitter {
protected:
~EventEmitter() = default;
EventEmitter(EventEmitter const&) = default;
EventEmitter(EventEmitter&&) = default;
EventEmitter& operator=(EventEmitter const&) = default;
EventEmitter& operator=(EventEmitter&&) = default;
By the way, in my example, the listeners would obtain a smart-pointer upon registering in order to listen, that they can destroy in order to stop listening.
I've seen this approach go awfully wrong, so be careful. Our engine at work uses a similar pattern and calls these smart pointers "receipts" which is cute I guess. If you go this route, at least make sure that these smart pointers are of a common type independent of the event type, so a caller can do something like:
vector<EventRecept> receipts;
receipts.push_back(window.listen<ClickEvent>(this, &MyClass::onClick));
receipts.push_back(window.listen<ScrollEvent>(this, &MyClass::onScroll));
// 12 other receipts
receipts.push_back(something.listen<AnotherEvent>(this, &MyClass::onAnother));
receipts.push_back(foo.listen<BarEvent>(this, &MyClass::onBar));
Otherwise your classes turn into a mess of event listener handle objects. That said, I rather despise the above pattern as well. It's insane to me to have to hold onto 20 distinct objects when the concept you're modeling is "clean up stuff when a lifetime ends."
For this reason, I very strongly prefer a lifetime handle approach. This is just an inversion of the above pattern: instead of _receiving_ a smart object when you register a handler, you _provide_ a smart object when you register a handler. Multiple handlers can be attached to the same smart object. Something like so:
Lifetime life;
window.listen<ClickEvent>(life, this, &MyClass::onClick);
If you have a class that has multiple lifetimes (e.g., a game state that knows the lifetime of the game session and the lifetime of a level) then it can have multiple Lifetime handles, but you only typically need one Lifetime whether you're registering for 1 or 500 things. Makes you objects smaller and more manageable.
Internally, the Lifetime can just be an object that holds a container of individual listener handles just like your original design, or the listeners can use the Lifetime to detect liveness. I've been leaning towards the later, giving us something like the following (using standard library types):
class Lifetime {
shared_ptr<int> _shared;
public:
weak_ptr<int> acquire() {
if (!_shared)
_shared = make_shared<int>(0);
return _shared;
}
void reset() {
_shared.reset();
}
};
template <typename Event>
class Emitter {
struct Listener {
weak_ptr<int> handle;
std::function<void(Event const&)> callback;
};
vector<Listener> _listeners;
public:
void listen(Lifetime& life, std::function<void(Event const&)> callback) {
_listeners.emplace_back(life.acquire(), move(callback));
}
void emit(Event const& event) {
for (auto& listener: _listeners)
if (listener.handle)
listener.callback(event);
_records.erase(remove_if(begin(_listeners), end(_listeners), [](auto& listener){ return !listener.handle; }), end(_listeners));
}
};
The result is that when Lifetime is destroyed or manually reset, all the listeners that were registered using that lifetime will become inert. They won't be cleaned up immediately but will instead be "garbage collected" later when the event is actually fired, but I haven't seen this ever be a problem; it would only be so if you registered many listeners on an event that never fired. Worst possible non-hypothetical problem I can see is if your wrapper std::function captured some shared ownership handle and kept the object alive too long, but that's why you should avoid shared pointers in public interfaces at all costs (note how its used only as an internal implementation detail in my sample).
The further bonus of the above system: much faster cleanup. With the handle-per-listen approach, if you hold onto N handles and then cleanup (say, each game object has 5 and you just destroyed a level with 100 game objects, giving you 500 handles!), you'll force the system to cleanup/deregister N listeners (likely invoking some kind of O(N) behavior each time, giving you O(NlogN) cleanup time!). With the lifetime handle system, your cleanup is effectively O(N). Of course, big-O can be misleading, so depending on exact use case you might find one better than the other in terms of absolute performance, but neither approach is ideal if perf is the most important thing for you. :)