Etant donné que le C++ est fortement typé, il est nécessaire de recourir aux templates afin de pouvoir gérer dynamiquement les événements et les écouteurs associés. Pour commencer, voici les deux classes de base (simplifiées) :

// le type d'événement
template <typename T>
class EventType {};
 
// l'événement lui-même
template <typename T>
class Event
{
public:
   const EventType<T>* type;
   Event(const EventType* type) : type(type) {}
 
protected:
   // méthode abstraite overridée par les
   // sous-classes
   virtual void notifyListener(const EventType<T>* eventType, T listener) = 0;
};

L'important est que la classe EventDispatcher ne soit pas un Template, sinon il ne sera possible que de propager un seul type d'événement. Il faut donc recourir aux templates de méthode pour parvenir à avoir cela :

class EventDispatcher
{
public:
 
   // le type du listener est lié à l'événement
   template <typename T>
   void addListener(const EventType<T>* type, T& listener);
};

Finalement, voici comment il est possible d'implémenter un événement qui pourra être propagé aux écouteurs :

class ActionEvent;
class ActionListener
{
public:
    // méthode qui sera invoquée
    virtual void onAction(ActionEvent* event) = 0;   
};
 
class ActionEvent : Event<ActionListener*>
{
public:
   static const EventType<ActionListener*>* ACTION;
   ActionEvent(const EventType<ActionListener*>* eventType) : Event<ActionListener*>(eventType) {}
 
protected:
 
    virtual void notifyListener(const EventType<ActionListener*>* eventType, ActionListener* listener)
    {
      listener->onAction(this);
    }
};
 
const EventType<ActionListener*>* ActionEvent::ACTION = new EventType<ActionListener*>();

Il ne reste plus qu'a monter un petit code de test pour voir ce qui se passe :

#include <iostream>
#include "events.hpp"
 
class MyListener : public ActionListener
{
public:
   virtual void onAction(ActionEvent* evt) { std::cout << "Listener called" << std::endl; }
};
 
int main()
{
   MyListener* listener = new MyListener();
   EventDispatcher* e = new EventDispatcher();
   e->addListener(ActionEvent::ACTION, listener);
   e->dispatchEvent(new ActionEvent(ActionEvent::ACTION));
 
   return 0;
}

Le compilateur nous lance une insulte pour le moins bizarre : il dit ne pas trouver de méthode addListener correspondant aux types spécifiés...

main.cpp: In function ‘int main()’:
main.cpp:51: error: no matching function for call to ‘EventDispatcher::addListener(const EventType<ActionListener*>*&, MyListener*)’

Hors, la classe MyListener ci-dessus hérite bien d'ActionListener et cela ne devrait donc pas poser de problème.

Le premier réflexe est de constater que l'utilisation d'un cast dynamique fonctionne !

e->addListener(ActionEvent::ACTION, (ActionListener*)listener);

Comme ceci est assez gênant, j'ai testé un appel similaire sur une classe template :

class Base {};
class Derived : public Base {};
 
template <typename T>
class MyTemplate
{
public:
   MyTemplate(T var1, T var2) {}
};
 
int main()
{
   Base* b = new Base();
   Derived* d = new Derived();
 
   // ok
   MyTemplate<Base*>* t1 = new MyTemplate<Base*>(d, b);
   MyTemplate<Base*>* t2 = new MyTemplate<Base*>(b, d);
   MyTemplate<Base*>* t3 = new MyTemplate<Base*>(b, b);
   MyTemplate<Base*>* t4 = new MyTemplate<Base*>(d, d);
 
   return 0;
}

Ci-dessus, le compilateur ne rechigne pas et tout fonctionne parfaitement. Maintenant, le même test, mais avec une méthode template :

class Base {};
class Derived : public Base {};
 
class MyTemplate
{
public:
   template <typename T>
   void test(T var1, T var2) {}
};
 
int main()
{
   Base* b = new Base();
   Derived* d = new Derived();
   MyTemplate* t = new MyTemplate();
 
   t->test(b, b); //ok
   t->test(d, d); //ok
   t->test(b, d); //ERROR
   t->test(d, b); //ERROR
 
   return 0;
}

Dans ce dernier exemple, le compilateur n'aime pas lorsque les types sont différents, même si la hiérarchie est correcte ! Pour contourner cela, il est possible de faire un cast comme je l'ai dit plus haut, mais cette solution devient vite pénible...

Il existe toutefois un moyen détourné de résoudre ce problème : utiliser deux types de templates ! Cela donne le code suivant :

template <typename T, typename S>
void EventDispatcher::addListener(const EventType<T>* type, S& listener)
{
   //...
}

Il se trouve que mon implémentation utilise une classe template à l'interne pour faire le mapping entre le type de l'événement et l'écouteur (cf pièce jointe) ! De ce fait, le typage est entièrement respecté au runtime et le compilateur est capable de détecter les erreurs à la compilation.

Merci à LiraNuna pour l'inspiration :)