Envoyé par
ImmoTPA
Hey Koala ! j'ai été pas mal occupé mais là j'essaie d'implémenter ce que tu m'as montré, j'en profite pour te poser deux ou trois autres questions si ça ne t'ennuie pas.
La première : est ce que je peux appliquer ta méthode avec tes foncteurs avec une fonction membre statique comme heapify ?
Désolé, j'arrive pas à suivre ton chemin de pensée, pourrais tu me donner un exemple de ce que tu veux faire :question
La seconde : j'ai lu suite à ce que tu m'as dit ici sur internet le fait que beaucoup de gens code des relations d'héritage à outrance. Pour ma part j'ai appris à l'école que quand on peut dire "A est un B" (comprendre, tout ce que peux faire et tout ce qu'est un B, un A peut le faire aussi et l'est aussi), alors une bonne implémentation consiste à faire hériter A de B. Ta définition diffère-t-elle ? si oui en quoi ?
Oui, même si l'idée véhiculée par ce que tu as appris s'y retrouve. Car, pour considérer un héritage publique, il faut en effet pouvoir dire qu'un objet de la classe dérivée EST-UN objet de la classe de base. Mais pas en terme de fonctionnalités; plutôt en terme de sémantique (comprend : de la définition que l'on trouve dans le dictionnaire) . Par exemple : la déclaration d'une voiture, d'une moto et d'un camion indique à chaque fois qu'il s'agit d'un véhicule. Cela te permet de franchir la première étape .
Mais ce n'est pas tout car il faut surtout respecter le Principe de Substitution de Liskov (Liskov Substitution Principle ou LSP en anglais):
Envoyé par
Barbara Liskov (trad wikipedia
Si q(x) est une propriété démontrable pour tout objet x de type T, alors q(y) est vraie pour tout objet y de type S tel que S est un sous-type de T.
C'est à dire que si ta classe de base expose une fonction faitMachinChose, il faut qu'il y ait du sens à appeler cette fonction depuis un objet de la classe dérivée.
Enfin, il faut respecter les règles de la programmation par contrat au minimum en ce qui concerne les préconditions et les postconditions:
- les préconditions ne peuvent pas être renforcées dans la classe dérivée et
- les postconditions ne peuvent pas être diminuées dans la classe de dérivée.
Ainsi, un rectangle dispose de trois invariants : quatre cotés, quatre angles droits et coté égaux 2 à 2. Or, on apprend à l'école "qu'un carré est un rectangle disposant de 4 cotés égaux". Si on se limite à la relation "EST-UN", on aura tendance à envisager de faire hériter Carre de Rectangle.
Mais, si on y réfléchit bien, Carré présente un invariant (4 cotés égaux) qui est beaucoup plus fort que l'invariant équivalent (coté égaux 2 à 2) auquel est soumis Rectangle. Et, comme un invariant est à la fois une précondition (c'est un condition qui doit être vérifiée avant toute tentative d'accès à notre objet) et une postcondition (elle doit être vérifiée après toute modification), on se rend compte le fait de faire hériter Carre de Rectangle contreviendrait à la première règle : une des préconditions serait renforcée dans la classe dérivée.
"Pas de problème" vas-tu me dire, "faisons hériter Rectangle de Carre"... Hé non, car, dans ce cas, la postcondition de Rectangles (coté égaux 2 à 2) serait diminuée par rapport à la postcondition équivalente de Carré (quatre cotés égaux).
C'est également la raison pour laquelle on ne peut pas faire hériter ListeTriee de Liste ou pour laquelle on ne peut sans doute pas faire hériter MinHeap (ou MaxHeap) de Heap : on trouvera surement un invariant dans ces classes qui dérogerait à l'une de ces règles
1 2 3
|
La troisième : pour les fonctions static apply : je ne peux pas accéder à mes fonctions membres par this->fonction vu que c'est une méthode statique, dois-je déclarer
static NodeT apply(NodeT n, MinHeap<Tkey>& heap) ? |
Pas MinHeap, mais plutôt Heap<T, ...> (car c'est définitivement la solution à préférer )
La quatrième (c'est la 3e fois que jmodifie le message) : comment je fais si dans ma structure PercolateDown la fonction apply doit appliquer apply de la structure BubbleDown ?
As tu remarqué que les fonctions statiques portent systématiquement le même nom : apply A vrai dire, j'aurais tout aussi bien pu créer un véritable foncteur qui n'aurait exposé que l'opérateur operator()(/* parametres */), cela serait revenu au même
Mais, quoi qu'il en soit, cela implique que tant que la signature de la fonction apply correspond à l'appel qui en est fait, tu peux décider de modifier la structure au départ de laquelle cette fonction est appelée "à ta guise" (on appelle cela un "trait" )
C'est la raison pour laquelle la solution la plus efficace consiste à ne pas utiliser l'héritage, mais à fournir un paramètre template désignant une structure exposant la fonction apply par fonction dont le comportement risque d'être redéfini.
Et s'il se fait que le comportement d'une des fonctions est en définitive identique à celui d'une structure qui a déjà été créé, rien ne t'interdit de faire du "recyclage", soit en utilisant directement cette structure comme paramètre template, soit en l'utilisant à l'intérieur d'une autre structure qui pourrait prendre la forme de
1 2 3 4 5 6 7
| template <typename T>
struct PercolateDown{
/* j'ai la flegme de vérifier le prototype de la fonction apply() pour cette structure ... */
bool apply(/* ... */){
return BubbleDown::apply(/* ... */) ;
}
}; |
L'énorme avantage de cette solution étant que tu crées effectivement une structure clairement distincte pour chaque utilisation, ce qui évitera au lecteur de ton code de se poser la question "mais pourquoi a-t-il donc fournit BubbleDown comme fonction à exécuter pour PercolateDown il a perdu la tête "... Mais les deux solutions sont tout à fait valides
La dernière : il y a souvent ce dilemme de savoir si on stock dans un std::vector par exemple plutôt une collection d'objets ou bien des pointeurs sur ces objets, pour ma part j'applique la règle, si les objets en question ont une raison d'être en dehors de la collection alors je mets des pointeurs sinon je stock directement les objets. Par exemple dans mon Heap, je stock des éléments, ces éléments n'existeraient pas si ils n'étaient pas voués à être stocké dans un Heap, je stock donc les éléments et non des pointeurs sur les éléments. Un contre-exemple serait par exemple, (j'invente un truc à la va-vite
) une classe vue comme un std::vector<*Etudiant>, les étudiants ayant probablement une autre raison d'être que celle d'appartenir à une classe (peut-être qu'ils peuvent aussi appartenir aussi à une chorale, un groupe de soutien, whatever).
Est-ce une bonne règle ? Si non, comment toi tu fais ?
Merci beaucoup
Je choisi de manière beaucoup plus simple, en fonction de la sémantique des éléments que je place dans la collection:
- S'ils ont sémantique de valeur, je crée une collection d'objet (ex : std::vector<Couleur> )
- S'ils ont sémantique d'entité, je crées une collection de pointeurs (de préférence intelligent) sur des objets (ex : std::vector<std::unique_ptr<Etudiants>>).
Après, il faut savoir que certains types ayant sémantique de valeur seront malgré tout très lourds à copier. Si l'on veut éviter la copie, on transmettra la collection sous la forme d'une référence éventuellement constante, si la fonction appelée ne doit pas en modifier le contenu (ex : void foo(std::vectr<Couleur> /* const */ & tab) ) voir, j'essayerai de fournir un intervalle représenté par deux itérateur à la fonciton (ex : template <typename ITER> void bar(ITER begin, ITER end), qui serait appelée sous la forme de bar(tab.begin(), tab.end()); )
Je préférerai d'ailleurs généralement la deuxième solution car elle apporte généralement une plus grande souplesse d'utilisation (on peut, par exemple, décider de changer le type de la collection "à notre guise", pour autant que le type d'itérateur reste compatible du moins, sans avoir à changer quoi que ce soit dans bar )
Partager