Double dispatch exemple C++

C’est un petit pattern, très simple, qui peut dans certaines situations vous éviter quelque saletés tel qu’un down-casting, du code déplacé ou autres copier-coller

Le problème:

Imaginons une classe ‘Personne’ qui ne possède qu’une fonction : discuteAvec(…)

La classe ‘Nommée’ en hérite et s’enrichie d’un nom.

La classe ‘Agée’ hérite elle aussi de ‘Personne’ mais celle-ci possède un age.

Maintenant, si vous êtes comme moi un adept du polymorphisme (sinon tant pis pour vous^), et que vous manipuler donc uniquement des ‘Personnes’, comment va-t-on gérer/accéder aux classes spécialisées dans notre fonction ‘disctuteAvec(…)’?

Le Double-Dispatch:
Le principe :

Notre classe de base ‘Personne’ va définir 3 fonctions virtuelles (pures):

* discuteAvec(Personne& personne) : Nous n’avons toujours pas d’informations sur le nom/age…
* discuteAvec(Nommee& nommee) : Nous pourrons accéder au nom de la personne.
* discuteAvec(Agee& agee) : Nous pourrons accéder à l’age de la personne.

Un peu de code serait probablement plus clair:

  1. class Nommee;
  2.  
  3. class Agee;
  4.  
  5. // Une personne
  6. class Personne
  7. {
  8. public:
  9.  
  10. // Fonction virtuelle pure: discute avec une personne dont
  11. // on ne connait ni le nom ni l'age.
  12. virtual void discuteAvec(Personne& personne) = 0;
  13.  
  14. // Fonction virtuelle pure surchargée: discute avec une personne qui a un nom
  15. // (ca arrive...)
  16. virtual void discuteAvec(Nommee& nommee) = 0;
  17.  
  18. // Fonction virtuelle pure surchargée: discute avec une personne qui a un age
  19. // (...parfois...)
  20. virtual void discuteAvec(Agee& agee) = 0;
  21. };
  22.  
  23. // Une personne qui a un nom
  24. class Nommee : public Personne
  25. {
  26. public:
  27.  
  28. // le nom
  29. std::string nom;
  30.  
  31. // Constructeur
  32. Nommee(const std::string& _nom):
  33. nom(_nom) {}
  34.  
  35. // on redéfinit les fonction virtuelle (voir implémentation)
  36. virtual void discuteAvec(Personne& personne);
  37. virtual void discuteAvec(Nommee& nommee);
  38. virtual void discuteAvec(Agee& agee);
  39. };
  40.  
  41. // Une personne qui a un age
  42. class Agee : public Personne
  43. {
  44. public:
  45.  
  46. // L'age
  47. unsigned short age; // (pas encore en 64 bits...)
  48.  
  49. // Constructeur
  50. Agee(const unsigned short _age):
  51. age(_age) {}
  52.  
  53. // on redéfinit les fonction virtuelle (voir implémentation)
  54. virtual void discuteAvec(Personne& personne);
  55. virtual void discuteAvec(Nommee& nommee);
  56. virtual void discuteAvec(Agee& agee);
  57. };

Ensuite, nos deux classe dérivées n’aurons qu’a redéfinir ‘discuteAvec(Nommee& nommee)’ et ‘discuteAvec(Agee& agee)’ pour gérer parfaitement tout ce qu’il y à gérer.

Et ‘discuteAvec(Personne& personne)’, ca ne sert a rien?!

Oh que si! C’est même la clef de voute de tout le pattern! Et cette fonction est pourtant très simple!

En fait, elle appelle juste la fonction ‘discuteAvec(*this)’ de ’personne’ (le paramêtre). Le type de l’objet (*this) est donc bien transmit et la fonction virtuelle ainsi appelée sera bien celle surchargée.

Implémentation des fonctions:

  1. // Quand on ne connait pas le nom ou l'age de la personne
  2. void Nommee::discuteAvec(Personne& personne)
  3. {
  4. // On appel SA fonction surchargée
  5. personne.discuteAvec(*this);
  6. }
  7. // Et cette même fonction, implémentée de manière indentique, sera
  8. // présente dans toutes les classes que l'on souhaite faire intéragir
  9.  
  10. // Quand on connait son nom
  11. void Nommee::discuteAvec(Nommee& nommee)
  12. {
  13. std::cout << nom << " discute avec " << nommee.nom << "." << std::endl;
  14. }
  15.  
  16. // Quand on connait son age
  17. void Nommee::discuteAvec(Agee& agee)
  18. {
  19. std::cout << nom << " discute avec quelqu'un qui a " << agee.age << " ans!" << std::endl;
  20. }
  21.  
  22. // Fonction identique à celle de la classe 'Nommee'
  23. void Agee::discuteAvec(Personne& personne)
  24. {
  25. personne.discuteAvec(*this);
  26. }
  27.  
  28. // Quand on connait son nom
  29. void Agee::discuteAvec(Nommee& nommee)
  30. {
  31. // La fonction est déja définit dans A
  32. nommee.discuteAvec(*this);
  33. }
  34.  
  35. // Quand on connait son age
  36. void Agee::discuteAvec(Agee& agee)
  37. {
  38. std::cout << "Quelqu'un de " << age << " ans discute avec quelqu'un qui a " << agee.age << " ans!" << std::endl;
  39. }

Un exemple:
  1. Personne* Inconnu_1 = new Nommee("Godefroi de Montmiraille");
  2. Personne* Inconnu_2 = new Nommee("Zigom@r");
  3. Personne* Inconnu_3 = new Agee(122);
  4. Personne* Inconnu_4 = new Agee(21);
  5.  
  6. Inconnu_1->discuteAvec(*Inconnu_2); // Zigom@r discute avec Godefroi de Montmiraille
  7. Inconnu_3->discuteAvec(*Inconnu_2); // Zigom@r discute avec quelqu'un qui a 122!
  8. Inconnu_1->discuteAvec(*Inconnu_4); // Godefroi de Montmiraille discute avec quelqu'un qui a 21!
  9. Inconnu_3->discuteAvec(*Inconnu_4); // Quelqu'un qui a 21 discute avec quelqu'un qui a 122!

PS: Vous noterez qu’il est important de passer les paramètres par pointeurs ou par références, ceci afin de présèrver le polymorphisme.