Hello,
J'aimerais imposer à un ensemble de classes l'implémentation d'une fonction dont la signature varie selon la classe.
Je suppose qu'il existe un pattern pour faire ça.
Une proposition à me faire ?
Merci.
Hello,
J'aimerais imposer à un ensemble de classes l'implémentation d'une fonction dont la signature varie selon la classe.
Je suppose qu'il existe un pattern pour faire ça.
Une proposition à me faire ?
Merci.
Qu'est-ce que tu peux imposer à tes classes? Sans sans moyen de pression, tu ne vas pas réussir.
CRTP comme d'habitude?
Il y a 16 classes filles. Les pointeurssur les instances sont stockées dans un conteneur, et la fameuse fonction est appelée de manière polymorphique par la suite.
Mais il y a une telle variabilité entre les implémentations des fonctions, que je me retrouve avec 8 arguments. 2 sont systématiquement utilisés. En général, c'est plutôt 3, au maximum 5.
C'est laid.
Avec le polymorphisme, c'est pour l'instant difficile. Il va falloir que je m'en débarrasse.
Salut,
En fait, il me semble qu'il y a, vraiment, un problème conceptuel dans ton histoire...
Par définition, une fonction virtuelle est une fonction dont le comportement va s'adapter au type réel de l'objet au travers duquel elle est appelée.
Mais, par définition, pour pouvoir redéfinir une fonction virtuelle, il faut que le nombre et le type de ses arguments soit stable :
Si, au niveau de la classe de base, tu définis une fonction virtuelle prenant un seul int en argument, le fait de déclarer et de définir une fonction dont le nom est identique mais qui nécessite deux entiers en arguments dans une classe dérivée ne va absolument pas redéfinir le comportement de la première au sens polymorphique du terme : cela va juste créer, pour la classe dérivée, une nouvelle fonction dont le nom est identique, mais qui nécessite deux arguments (et dont le comportement pourra être redéfini au besoin par les classes qui dériveront de ta classe dérivée).
Une des solutions consiste à déclarer, dans ta classe de base, ta fonction avec "le maximum de paramètres possibles" et à fournir une valeur par défaut pour les paramètres "qui ne sont pas forcément utilisés".
Mais cela implique que:
De plus, si un nombre de trois arguments est un nombre "correct" (on peut encore assez facilement se souvenir de l'ordre dans lequel il faut les passer), un nombre de cinq à huit arguments me semble pour le moins excessif, car on risque toujours de se tromper dans l'ordre des arguments et, s'ils sont de type compatible (des entiers par exemple), il devient assez facile de filer la mauvaise valeur à l'un d'eux, ce qui risque de fausser quelque peu les résultats
- Pour chaque paramètre qui risque de ne pas être utilisé, il soit possible de définir une valeur qui puisse représenter un élément invalide
- Dans chaque (re)définition de la fonction, tu devras tester l'ensemble des paramètres "optionnels" pour t'assurer de leur (in)validité et donc savoir s'il respectent les conditions d'utilisation propres à la redéfinition du comportement.
Dés lors, je me demande si la solution (basée sur une approche purement OO) ne nous serait pas, une fois de plus, donnée par David Wheeler : ajouter une abstraction permettant de représenter une "liste d'arguments".
L'idée de base serait -- peut être -- de créer une classe de base, mettons ArgumentList, qui serait transmise sous la forme d'une référence (éventuellement constante) à ta fonction virtuelle, qui serait dérivée en "spécialisations" permettant de représenter trois, quatre, cinq, six, sept ou huit arguments.
Dans ce cas, le double dispatch risque de poser des problème, car il en faudrait deux : un premier pour récupérer le type réel de l'objet au départ duquel tu appelle la fonction virtuelle et l'autre pour récupérer le type réel de la liste d'arguments. Il est possible d'y arriver, mais une chose est sure, le patron de conception visiteur n'est absolument pas adapté à la manoeuvre
Ceci dit, on déconseille généralement d'avoir recours aux transtypage (que ce soit static_cast ou dynamic_cast) afin de "downcaster" une référence (ou un pointeur) vers le type de base. Et ce conseil est tout ce qu'il y a de plus valide en première approche.
La raison de se conseil est que, sinon, il est très facile de finir par se retrouver avec un code proche de
qui ne sera absolument pas maintenable sur le long terme.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 if( dynamic_cast<Type1*>(¶m)){ /* ... */ }else if( dynamic_cast<Type2*>(¶m)){ /* ... */ }else if( dynamic_cast<Type3*>(¶m)){ /* ... */ }/* ... */ else if( dynamic_cast<TypeN*>(¶m)){ /* ... */ }
En effet, un tel code aurait très facilement tendance à se "multiplier comme des petits pains", et toute tentative d'ajouter un nouveau type dérivé nécessiterait de modifier chaque apparition de ce code afin de prendre le nouveau type dérivé en compte.
Par contre, si tu sais que la redéfinition de la fonction pour l'une de tes classes dérivées a besoin strictement de cinq arguments (et non de 4 ou de 6), et que tu peux donc essayer de transformer l'argument qu'elle reçoit dans un type particulier en étant sur que, si le transtypage ne fonctionne pas, c'est qu'il y a un problème, le transtypage peut te permettre d'éviter le recours à un double dispatch supplémentaire.
A ce moment là, tu n'es plus dans une logique de sélection du type comme celle que met en oeuvre le code que je viens de montrer, mais bien dans une logique de vérification du type qui t'es transmis
Tu pourrais donc parfaitement envisager d'avoir une hiérarchie de classes pour tes arguments qui prendrait la forme de
d'un coté, et, de l'autre, ta hiérarchie de 16 classes qui prendrait la forme de
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 class ArgumentList{ /* ... */ }; /* pour trois arguments */ class ThreeArgumentList : public ArgumentList{ public: Type1 arg1; Type2 arg2; Type3 arg3; }; /* pour quatre arguments */ class FourArgumentList : public ArgumentList{ public: Type1 arg1; Type2 arg2; Type3 arg3; Type4 arg4; }; /* ... */ /* pour 8 arguments */ class HeightArgumentList : public ArgumentList{ public: Type1 arg1; Type2 arg2; /* ... */ Type8 arg8; };
Dans un tel cas, le transtypage n'est peut être pas la meilleure solution (il faudrait comparer en terme de performances par rapport à un double dispatch classique), mais il reste malgré tout une alternative tout à fait valable
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 class Base{ public: virutal void foo(ArgumentList /* const */ &) = 0; }; /* cette classe-ci a strictement besoin de trois arguments */ class Derivee1 : public Base{ public: void foo(ArgumentList /* const */ & args){ ThreeArgumentList * real = dynamic_cast<ThreeArgumentList*>(&args); if(!real){ /* ... */ }else{ /* ce n'est pas ce qu'on attendait, faut l'indiquer à l'utilisateur (ou * au dévelopeur) */ } } } /* et celle-ci a strictement besoin de huit arguments */ class DeriveeN : public Base{ public: void foo(ArgumentList /* const */ & args){ ThreeArgumentList * real = dynamic_cast<HeightArgumentList*>(&args); if(!real){ /* ... */ }else{ /* ce n'est pas ce qu'on attendait, faut l'indiquer à l'utilisateur (ou * au dévelopeur) */ } } }![]()
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
Je suis pas sur de tout avoir compris avec ton histoire 8 paramètres mais tous ne servent pas.
On pourrait avoir un bout de code (meme fictif) pour voir clairement la situation et ce que tu veux faire ?
"Never use brute force in fighting an exponential." (Andrei Alexandrescu)
Mes articles dont Conseils divers sur le C++
Une très bonne doc sur le C++ (en) Why linux is better (fr)
Pour simplifier à l'extrême, disons que j'ai une classe Quadrilatère, avec les classes filles Rectangle et Carré. Des instances de chacune des classes filles sont stockées sous forme de pointeur dans un conteneur, et de manière polymorphique, je veux appeler une méthode Redimensionner(). Selon la classe sous-jacente, le nombre d'arguments nécessaire ne sera pas le même. Ma méthode, virtuelle, a donc 2 arguments, même si l'un d'enter eux ne sera pas utilisé dans le cas du carré.
Il y clairement un problème de conception à la base, mais je ne peux pas complètement renverser la table.
Bon, je vais prendre à présent du temps pour lire la réponse de koala01.![]()
C'est là qu'est l'os, hélas.
Pour le début de ton intervention, nous sommes biens d'accord.
Pour la suite, le problème est que le code concerné est sensible aux performances. En fait, une partie des arguments serait regroupable par paires (les structures de données existent déjà). Je pourrais me retrouver avec 5 arguments plutôt que 8. Mais construire les objets prendrait du temps sans que cela ait un apport fonctionnel.
Alors utiliser ajouter une indirection et utiliser du dynamic_cast...
Je crois que je me repencherai dessus plus tard quand j'aurai un peu élagué ce code mal gaulé et assez confu. J'ai encore du mal à en avoir une vision macro permettant de redéfinir une archi "éclairée".
Un truc a base de variadic template, ca peut pas le faire ?
le soucis avec cette methode, c'est que tu as la moitie des fonctions qui ne servent a rien et ca fait du code mort.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 #include <iostream> using namespace std; struct poly { template <class ...Args> void foo(Args... a) { resize(a...); } private : virtual void resize(int,int)=0; virtual void resize(int i)=0; }; struct Carre : poly { private : virtual void resize(int,int){} virtual void resize(int i){std::cout<<"Carre int"<<std::endl;} }; struct Rect : poly { private : virtual void resize(int,int){std::cout<<"Rect 2"<<std::endl;} virtual void resize(int i){} }; int main() { Carre b; poly* a=&b; a->foo(4); a->foo(4,5); Rect c; a=&c; a->foo(4,2); }
P-e qu'avec du CRTP ya moyen de diminuer ca.
"Never use brute force in fighting an exponential." (Andrei Alexandrescu)
Mes articles dont Conseils divers sur le C++
Une très bonne doc sur le C++ (en) Why linux is better (fr)
Je n'ai pas dit qu'il fallait utiliser cette technique partout
J'ai dit que, de manière exceptionnelle, le fait d'avoir une telle structure permettait de rendre ta liste de paramètres plus cohérente
Et surtout, si tu es sensisble aux performances, j'ai envie de dire qu'il y a, de toutes manières bien des possibilités à explorer au niveau de l'algorithme, avant de t'inquiéter d'une indirection supplémentaire
L'idée ici est d'utiliser l'héritage plus comme un moyen d’agréger des données que comme un moyen de profiter du polymorphisme
Ceci dit, si les valeurs sont toutes de même type, rien ne t'empêche de carrément passer un std::vector par référence, même si on en revient au problème de base : la difficulté de savoir à quoi correspond la quatrième valeur
Un static_cast ou un cast C style pourrait parfaitement suffire égalementAlors utiliser ajouter une indirection et utiliser du dynamic_cast...
Le problème, c'est que cela laisse plus facilement le développeur faire des bêtises (et risquer de passer la structure à 3 arguments quand il faut la structure à 5)
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
Un peu trop, je comprends pas comment ce qui suit est possible:
Ou tu fais un appel polymorphe et tu es incapable de passer un nombre d'arguments qui dépend de la classe la plus dérivée, ou tu ne fais pas un appel polymorphe. Dans les deux cas, je ne vois pas quel est le problème.de manière polymorphique, je veux appeler une méthode Redimensionner(). Selon la classe sous-jacente, le nombre d'arguments nécessaire ne sera pas le même.
sans préjuger de tes contraintes, tu es sûr de ne pas pouvoir redéfinir la notion de redimensionnement de façon uniforme (facteur d'échelle, ou cefX + coefY [+ coef Z])? Sinon où est le polymorphisme là dedans? Si tu appelles de manière ensembliste sur un container, tu as bien un paramètre global, mais si tu appelles forme par forme pour définir le redimensionnement, alors WFT le container et le polymorphisme?
Mais comment diantre cet appel polymorphe à tiroir et à piston est-il actuellement fait?
Si les paramètres doivent être redéfinis pour chaque forme quel est l'attendu global?
L'attendu global, c'est le calcul d'un coût. Son calcul dépend d'un nombre variable d'arguments selon l'objet sur lequel se fait le calcul. Et la valeur de ces arguments n'est pas connue au moment de la construction des instance et leur insertion dans le conteneur.
Exemple débile :
Disons que tu tiens une sandwicherie. Tu veux mettre à jour le prix de ta carte toutes les demi-heures en fonctions du prix des ingrédients côtés en bourse. Chaque type de sandwich pourrait avoir une fonction updatePrice().
Dès que tu reçois l'ensemble des prix de tes ingrédients, tu parcours l'ensemble des sandwiches de ta carte (le conteneur), et tu appelles updatePrice().
Sauf qu'il y a des sandwiches jambon-beurre, jambon-emmental, jambon_cru-reblochon, tomates-mozarelle, etc...
Tu peux appeler séquentiellement les updatePrice() avec leur liste de prix spécifique, mais on se retrouve alors dans le problème de maintenance évoqué par koala01 avec ses dynamic_cast.
Cet exemple n'a strictement rien de commun avec l'exemple que tu présentais avant!
Ici, il n'y a strictement rien qui t'empêche d'avoir la liste des différents prix, ni aux sandwiches de savoir exactement de quel ingrédients ils ont besoin, et en quelle quantité.
Si donc tu as la liste des prix (en € au kilo) pour les matières premières PriceList, qui permet de récupérer le prix de chaque élément utilisé (jambon, parme, mayonnaise, beurre, fromage, ...), tu peux parfaitement transmettre simplement cette liste à ta fonction updatePrice.
A partir de là, chaque sandwich peut adapter son comportement sous une forme qui serait proche de
Et, comme tu auras sans doute aussi utilisé un tout petit peu le patron de conception "décorateur" (parce qu'un jambon beurre peut être agrémenté de quelques trucs), cela pourrait tout aussi prendre une forme proche de
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 void JambonBeurre::updatePrice(PriceList const & pl){ double jambon = pl.jambon() / 1000 * quantite * marge; double beurre = pl.beurre() / 1000 * quantite * marge; double pain = pl.painBlanc() / 1000 * quantite * marge; price_ = (jambon + beurre + pain) * tva; }
Au final, on n'a plus qu'un et un seul argument (une structure, soit, mais un seul argument quand même), dont chaque classe dérivée va chercher "la valeur" qui l'intéresse et la manipule en fonction des besoins qui lui sont propres
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 void JambonBeurre::updatePrice(PriceList const & pl){ jambon.computePrice(pl); // la part de jambon calcule son prix // (en fonction de la quantité dont elle a besoin) beurre.computePrice(pl); // idem pour le beurre pain.computePrice(pl); // idem pour le pain /*... */ price_ = computeTotal(); // calcule le total de toutes les parts }
Tu peux donc sans aucun problème utiliser le polymorphisme et envisager de faire un traitement global sur tous tes objets, basé sur une liste de prix clairement définie![]()
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
1/ De toute manière, je n'hardcode pas les ingrédients dans ce genre de conception
2/ Tes sandwitch sont des observeurs du prix de chacun des ingrédients et ils sont notifiés des modifications, t'a pas à parcourir une liste de tous les sandwitchs.
3/ Je ne vois toujours pas quel est le problème à résoudre même avec une autre conception. Ou tu filtres ta liste en fonction des prix modifiés, ou tu laisses les objets ignorer les notifications pour les matières qui ne les intéressent pas ou tes objets se recalculent de toute manière systématiquement -- pour le nombre de sandwitchs sur une carte, ça ne vaut pas la peine de mettre en place quelque chose de plus complexe.
Partager