Notez que les équations dans ce document sont affichées à l'aide de MathJax.
Il existe des problèmes pour lesquels l'approche polymorphique classique, exprimée sous la forme sujet.action(objet) où sujet est une abstraction polymorphique (p. ex. : une indirection vers une Forme, menant effectivement vers un Cercle, un Carré, un Triangle ou une autre sorte de forme) ne convient pas.
Ces cas, qu'on identifie en anglais sous le vocable Multiple Dispatch (mais le cas le plus fréquent est le polymorphisme sur deux types, ou Double Dispatch) sont des opérations (action(), dans notre écriture) tiennent compte du type effectif non seulement de l'instance réputée active (sujet dans notre écriture plus haut) mais bien de celle de plusieurs types (par exemple, le type effectif d'objet dans notre écriture). Le vocable technique utilisé pour des services polymorphiques sur la base de plusieurs types est multiméthode.
Le cas canonique est celui de la détection de collisions entre deux volumes. L'exemple qui suit est minimaliste, simpliste et incomplet, mais montre la structure générale d'une solution à ce problème en C++. Ce n'est pas une explication complète de ce que sont les multiméthodes et de diverses alternatives (je couvre ça en classe, habituellement).
Supposons Volume une abstraction polymorphique dont il existe au moins deux spécialisations (pour les fins de l'exemple, Boite et Sphere seront toutes deux des spécialisations de Volume).
Supposons que v0 et v1 soient deux volumes. Les cas possibles sont :
Sans surprises, ajouter des types de spécialisations de Volume (comme c'est le cas dans de vrais projets) accroît de beaucoup le nombre de cas possibles ( cas à traiter pour types de spécialisations, quoique dans un cas comme celui des collisions, plusieurs des cas soient redondants – collisionner une Boite et une Sphere implique le même algorithme que collisionner une Sphere et une Boite, et peut s'implémenter par un simple relais), ce qui démontre que ce problème est un véritable problème de design.
Nous souhaitons savoir si v0 et v1 collisionnent (si leurs volumes s'intersectent). Il se trouve que les algorithmes pour détecter une collision entre deux sphères, entre deux boîtes, ou entre une sphère et une boîte sont différents. Connaître les types effectifs des deux volumes permet de choisir le meilleur algorithme dans les circonstances.
En gros l'idée de base derrière ce qui suit est que :
Le coût à l'exécution de cette manoeuvre est une double indirection polymorphique, ce qui peut être dispendieux. Il y a aussi un coût architectural (ajouter un nouveau type dans le système est complexe).
Pour exprimer nos volumes, nous aurons recours à une classe Point. J'ai choisi une implémentation pour espaces tridimensionnels, l'idée étant de discuter de collisions entre volumes, et j'ai utilisé des nombres à virgule flottante de simple précision pour les coordonnées, mais ce sont des choix d'implémentation, pas des obligations. L'implémentation est somme toute assez simple pour un exemple comme celui-ci. |
|
Le Volume sera la racine polymorphique de notre exemple. Notez les déclarations a priori des classes Sphere et Boite. En effet, une version spécialisée de la signature de notre multiméthode doit être déclarée dès la racine polymorphique, et spécialisée éventuellement dans tous les enfants. Conséquemment, ajouter une classe dans le système implique au moins (a) l'ajout d'une déclaration a priori à même la racine polymorphique, (b) l'ajout d'une signature de méthode abstraite dès la racine polymorphique et, bien entendu, (c) l'ajout d'une implémentation de cette nouvelle méthode dans chaque classe du système. L'entretien du code d'un système de multiméthodes est relativement lourd. |
|
Une Boite sera une sorte de Volume. Nous utiliserons un std::array pour entreposer les points qui en constitueront les coins. Notez que l'implémentation proposée ici est banale et incomplète. Je vous invite à l'enrichir à la hauteur de vos souhaits. En particulier, ce que signifie « boîte par défaut » est éminemment politique, alors vous comprendrez que le choix fait ici soit discutable. |
|
Une Sphere sera aussi un Volume somme toute banal. Ce qui est sympathique avec les sphères est, de toute manière, leur simplicité. |
|
Nous utiliserons un dépôt d'algorithmes de détection de collisions pour les regrouper et réduire la répétition de code. On y trouvera déclarations pour types de volumes dont il faudra tenir compte. Il est toutefois possible de réduire le couplage entre fichiers en utilisant des déclarations a priori, comme nous le faisons ici. Le recours à un dépôt d'algorithmes est un choix de design, pas une nécessité. |
|
Pour l'implémentation des algorithmes, on pourra toutefois réduire les cas possible en implémentant certaines définitions en termes d'autres définitions sur les mêmes types (ici, le test sur Boite et Sphere délègue au test sur Sphere et Boite). Je ne prétends pas que les implémentations de tests de collision proposées ici soient efficaces (ou même correctes); en fait, elles le sont pour le cas d'une collision entre deux sphères, mais pour le reste, j'ai écrit un petit quelque chose rapidement et je n'ai rien validé, alors à vos risques et périls! |
|
Enfin, un petit programme de test utilise un conteneur de quelques volumes et les teste pour découvrir des collisions. Rien de transcendant, soit, mais ça montre tout de même que l'ensemble fonctionnet et fait le travail. |
|
Le problème peut être significativement simplifié en utilisant des techniques et idiomes plus contemporains, en particulier le type std::variant de C++ 17. Pour une vision sommaire :
Pour que l'exemple demeure simple et se limite à la mécanique, supposons deux classes vides (purement symboliques) Sphere et Boite, et définissions Volume comme l'un de ces types (à l'aide de std::variant). Détecter une collision entre deux volumes, plutôt que d'impliquer une découverte polymorphique de types de complexité linéaire en fonction du nombre de types impliqués, devient une multivisite du std::variant, ce qui mène à un appel (presque) direct de l'algorithme approprié. On conviendra que le gain de simplicité est... appréciable. |
|
Quelques liens pour enrichir le propos.