Appel de la bonne fonction dans une hiérarchie de classe
4 réponses
Alexis Guillaume
Bonjour =E0 tous.
Je dispose d'une hi=E9rarchie de classe o=F9 chaque classe sait g=E9rer
certains types de messages provenant du r=E9seau. Bien s=FBr lorsque ces
messages me parviennent, je ne dispose que de pointeur sur la classe
de base.
Je suis donc =E0 la recherche d'une solution pour que la bonne fonction
soit appel=E9e quand j'=E9cris du code du genre :
Base *base =3D ....;
base->traite_message( type_message, message ) // (a)
Et ce en fonction du type de message.
Une premi=E8re solution est de rendre traite_message virtuelle. Chaque
classe de la hi=E9rarchie *doit* l'impl=E9menter ainsi :
void ClasseDerivee::traite_message( std::string const &type_message,
Msg
message ) {
if (/* cette classe sait g=E9rer ce type de message */) {
// gestion du message
} else {
ClasseParente::traite_message( type_message, message );
}
}
Ainsi, =E0 chaque appel de traite_message, on est s=FBr que l'on passera
forc=E9ment par la classe qui sait traiter ce message.
Je trouve cette solution un peu bof. Ce qui m'ennuie surtout c'est
qu'il va =EAtre tr=E8s facile de faire des erreurs dans le code, en se
trompant de classe parente par exemple, ou en oubliant de surchager
traite_message() dans une classe qui n'en traiterait aucun, ce qui
aurait quand m=EAme pour effet de casse les appels en cha=EEnes =E0
traite_message().
Du coup, j'ai cherch=E9 une solution =E0 base de map et de pointeurs de
fonction : La classe de base va g=E9rer une map associant std::string
(suppos=E9 =EAtre un type de message) et pointeurs sur fonctions membres.
La m=E9thode traite_message, non virtuelle, va utiliser cette map pour
appeler la bonne m=E9thode.
Base *base1 =3D new Base();
Base *base2 =3D new Derived1();
Base *base3 =3D new Derived2();
base1->traite_message( "undefined" ); // not found
base1->traite_message( "func_base" ); // ok
base1->traite_message( "func_derived" ); // not found
base2->traite_message( "func_base" ); // ok
base2->traite_message( "func_derived" ); // ok
base2->traite_message( "func_derived2" );// not found
base3->traite_message( "func_derived2" );// ok
delete base1;
delete base2;
delete base3;
}
------------------------- fin code source --------------------------
Cette m=E9thode semble fonctionner, mais j'ai un doute, notamment =E0
cause de la conversion que je dois faire aux lignes indiqu=E9e par des
ast=E9risques. Cette conversion de void (*Derived::)(std::string const
&) =E0 void (*Base::)( std::string const &) est-elle l=E9gale ?
Plus g=E9n=E9ralement, que pensez vous de ces deux mani=E8res de faire ?
Avez vous une pr=E9f=E9rence entre les deux, voire pour une troisi=E8me
m=E9thode que je n'ai pas vue ?
Bonne lecture et bon week-end =E0 tous,
Alexis Guillaume.
Cette action est irreversible, confirmez la suppression du commentaire ?
Signaler le commentaire
Veuillez sélectionner un problème
Nudité
Violence
Harcèlement
Fraude
Vente illégale
Discours haineux
Terrorisme
Autre
James Kanze
On Oct 3, 11:33 am, Alexis Guillaume wrote:
Je dispose d'une hiérarchie de classe où chaque classe sait gérer certains types de messages provenant du réseau. Bien sûr lorsque ces messages me parviennent, je ne dispose que de pointeur sur la classe de base.
Je suis donc à la recherche d'une solution pour que la bonne fonction soit appelée quand j'écris du code du genre :
Base *base = ....; base->traite_message( type_message, message ) // (a)
Et ce en fonction du type de message.
Une première solution est de rendre traite_message virtuelle. Chaque classe de la hiérarchie *doit* l'implémenter ainsi :
void ClasseDerivee::traite_message( std::string const &type_message, Msg m essage ) { if (/* cette classe sait gérer ce type de message */) { // gestion du message } else { ClasseParente::traite_message( type_message, message ); } }
Ainsi, à chaque appel de traite_message, on est sûr que l'on passera forcément par la classe qui sait traiter ce message.
Je trouve cette solution un peu bof.
C'est cépendant une des solutions classiques. Surtout quand le type de message est encodé à part. Sinon, on fait des messages même un type polymorphique, avec une classe dérivée par type de message, et on implémente l'idiome de « double dispatch ». À peu près :
Ensuite, dans ton hièrarchie, chaque classe implemente les fonctions de notification qui l'intéressent.
Ce qui m'ennuie surtout c'est qu'il va être très facile de faire des erreurs dans le code, en se trompant de classe parente par exemple, ou en oubliant de surchager traite_message() dans une classe qui n'en traiterait aucun, ce qui aurait quand même pour effet de casse les appels en chaînes à traite_message().
Je me démande si une partie du problème n'est pas que tu as conçu trop de niveaux. Je vois bien une classe de base abstraite, du genre MessageHandler, ci-dessus, mais je ne vois pas trop l'intérêt de créer une hièrarchie aussi profonde. En général, il y a l'interface (la classe de base abstraite, qui n'implémente rien), et ensuite, ses diverses implémentations, tout à un niveau, ou éventuellement deux, si l'implémentation utilise le modèle de template (« template method pattern », à ne pas confondre avec les templates C++). Il y a bien sûr des exceptions, mais à ta place, je m'assurerais que chaque niveau correspond bien à un concepte signifiant. Donc, par exemple, en reprenant ton exemple, à quel concepte correspondrait une classe qui s'intéresserait à des informations joueurs, mais non aux mouvements. Reduire la profondeur reduira nettement les endroits où une erreur telle que tu décris pourrait se produire. (S'il n'y a qu'une classe dérivée à partir d'une base totalement abstraite, il n'y a pas besoin d'appeler le parent. Et s'il n'y en a que deux, la deuxième ne peut pas trop se tromper en ce qui concerne le parent à appeler.)
Du coup, j'ai cherché une solution à base de map et de pointeurs de fonction : La classe de base va gérer une map associant std::string (supposé être un type de message) et pointeurs sur fonctions membres.
C'est faisable, mais c'est lourd. Et ça ne vaut que rarement la peine.
Une des premières bibliothèques de fenêtrage, XViews, utlisait quelque chose de ce genre. On s'inscrivait pour les types de message intéressant, en passant le pointeur this et l'adresse d'une fonction membre. La solution que je viens de décrire me semble plus robuste et plus simple à mettre en oeuvre.
La méthode traite_message, non virtuelle, va utiliser cette map pour appeler la bonne méthode.
------------------------- fin code source --------------------------
Cette méthode semble fonctionner, mais j'ai un doute, notamment à cause de la conversion que je dois faire aux lignes indiquée par des astérisques. Cette conversion de void (*Derived::)(std::string const &) à void (*Base::)( std::string const &) est-elle légale ?
Tu veux dire "void (Derived::*)( std::string const& )" à "void (Base::*)( std::string const& )". Ça marche. (Je l'ai vérifié quand j'ai rencontré XViews.) Mais à mon avis, c'est une invitation à l'erreur (comme la plupart des conversions).
Plus généralement, que pensez vous de ces deux manières de faire ? Avez vous une préférence entre les deux, voire pour une troisième méthode que je n'ai pas vue ?
Franchement, je préfère la double dispatch (qui s'apparente au modèle de visiteur) ; sinon, ta première solution n'est pas vraiment mauvause non plus.
-- James Kanze (GABI Software) email: Conseils en informatique orientée objet/ Beratung in objektorientierter Datenverarbeitung 9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
On Oct 3, 11:33 am, Alexis Guillaume <alexis.c.guilla...@gmail.com>
wrote:
Je dispose d'une hiérarchie de classe où chaque classe sait
gérer certains types de messages provenant du réseau. Bien sûr
lorsque ces messages me parviennent, je ne dispose que de
pointeur sur la classe de base.
Je suis donc à la recherche d'une solution pour que la bonne fonction
soit appelée quand j'écris du code du genre :
Base *base = ....;
base->traite_message( type_message, message ) // (a)
Et ce en fonction du type de message.
Une première solution est de rendre traite_message virtuelle.
Chaque classe de la hiérarchie *doit* l'implémenter ainsi :
void ClasseDerivee::traite_message( std::string const &type_message,
Msg
m essage ) {
if (/* cette classe sait gérer ce type de message */) {
// gestion du message
} else {
ClasseParente::traite_message( type_message, message );
}
}
Ainsi, à chaque appel de traite_message, on est sûr que l'on
passera forcément par la classe qui sait traiter ce message.
Je trouve cette solution un peu bof.
C'est cépendant une des solutions classiques. Surtout quand le
type de message est encodé à part. Sinon, on fait des messages
même un type polymorphique, avec une classe dérivée par type de
message, et on implémente l'idiome de « double dispatch ». À
peu près :
Ensuite, dans ton hièrarchie, chaque classe implemente les
fonctions de notification qui l'intéressent.
Ce qui m'ennuie surtout c'est qu'il va être très facile de
faire des erreurs dans le code, en se trompant de classe
parente par exemple, ou en oubliant de surchager
traite_message() dans une classe qui n'en traiterait aucun, ce
qui aurait quand même pour effet de casse les appels en
chaînes à traite_message().
Je me démande si une partie du problème n'est pas que tu as
conçu trop de niveaux. Je vois bien une classe de base
abstraite, du genre MessageHandler, ci-dessus, mais je ne vois
pas trop l'intérêt de créer une hièrarchie aussi profonde. En
général, il y a l'interface (la classe de base abstraite, qui
n'implémente rien), et ensuite, ses diverses implémentations,
tout à un niveau, ou éventuellement deux, si l'implémentation
utilise le modèle de template (« template method pattern », à
ne pas confondre avec les templates C++). Il y a bien sûr des
exceptions, mais à ta place, je m'assurerais que chaque niveau
correspond bien à un concepte signifiant. Donc, par exemple, en
reprenant ton exemple, à quel concepte correspondrait une classe
qui s'intéresserait à des informations joueurs, mais non aux
mouvements. Reduire la profondeur reduira nettement les
endroits où une erreur telle que tu décris pourrait se produire.
(S'il n'y a qu'une classe dérivée à partir d'une base totalement
abstraite, il n'y a pas besoin d'appeler le parent. Et s'il n'y
en a que deux, la deuxième ne peut pas trop se tromper en ce qui
concerne le parent à appeler.)
Du coup, j'ai cherché une solution à base de map et de
pointeurs de fonction : La classe de base va gérer une map
associant std::string (supposé être un type de message) et
pointeurs sur fonctions membres.
C'est faisable, mais c'est lourd. Et ça ne vaut que rarement la
peine.
Une des premières bibliothèques de fenêtrage, XViews, utlisait
quelque chose de ce genre. On s'inscrivait pour les types de
message intéressant, en passant le pointeur this et l'adresse
d'une fonction membre. La solution que je viens de décrire me
semble plus robuste et plus simple à mettre en oeuvre.
La méthode traite_message, non virtuelle, va utiliser cette
map pour appeler la bonne méthode.
------------------------- fin code source --------------------------
Cette méthode semble fonctionner, mais j'ai un doute,
notamment à cause de la conversion que je dois faire aux
lignes indiquée par des astérisques. Cette conversion de void
(*Derived::)(std::string const &) à void (*Base::)(
std::string const &) est-elle légale ?
Tu veux dire "void (Derived::*)( std::string const& )" à "void
(Base::*)( std::string const& )". Ça marche. (Je l'ai vérifié
quand j'ai rencontré XViews.) Mais à mon avis, c'est une
invitation à l'erreur (comme la plupart des conversions).
Plus généralement, que pensez vous de ces deux manières de
faire ? Avez vous une préférence entre les deux, voire pour
une troisième méthode que je n'ai pas vue ?
Franchement, je préfère la double dispatch (qui s'apparente au
modèle de visiteur) ; sinon, ta première solution n'est pas
vraiment mauvause non plus.
--
James Kanze (GABI Software) email:james.kanze@gmail.com
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Je dispose d'une hiérarchie de classe où chaque classe sait gérer certains types de messages provenant du réseau. Bien sûr lorsque ces messages me parviennent, je ne dispose que de pointeur sur la classe de base.
Je suis donc à la recherche d'une solution pour que la bonne fonction soit appelée quand j'écris du code du genre :
Base *base = ....; base->traite_message( type_message, message ) // (a)
Et ce en fonction du type de message.
Une première solution est de rendre traite_message virtuelle. Chaque classe de la hiérarchie *doit* l'implémenter ainsi :
void ClasseDerivee::traite_message( std::string const &type_message, Msg m essage ) { if (/* cette classe sait gérer ce type de message */) { // gestion du message } else { ClasseParente::traite_message( type_message, message ); } }
Ainsi, à chaque appel de traite_message, on est sûr que l'on passera forcément par la classe qui sait traiter ce message.
Je trouve cette solution un peu bof.
C'est cépendant une des solutions classiques. Surtout quand le type de message est encodé à part. Sinon, on fait des messages même un type polymorphique, avec une classe dérivée par type de message, et on implémente l'idiome de « double dispatch ». À peu près :
Ensuite, dans ton hièrarchie, chaque classe implemente les fonctions de notification qui l'intéressent.
Ce qui m'ennuie surtout c'est qu'il va être très facile de faire des erreurs dans le code, en se trompant de classe parente par exemple, ou en oubliant de surchager traite_message() dans une classe qui n'en traiterait aucun, ce qui aurait quand même pour effet de casse les appels en chaînes à traite_message().
Je me démande si une partie du problème n'est pas que tu as conçu trop de niveaux. Je vois bien une classe de base abstraite, du genre MessageHandler, ci-dessus, mais je ne vois pas trop l'intérêt de créer une hièrarchie aussi profonde. En général, il y a l'interface (la classe de base abstraite, qui n'implémente rien), et ensuite, ses diverses implémentations, tout à un niveau, ou éventuellement deux, si l'implémentation utilise le modèle de template (« template method pattern », à ne pas confondre avec les templates C++). Il y a bien sûr des exceptions, mais à ta place, je m'assurerais que chaque niveau correspond bien à un concepte signifiant. Donc, par exemple, en reprenant ton exemple, à quel concepte correspondrait une classe qui s'intéresserait à des informations joueurs, mais non aux mouvements. Reduire la profondeur reduira nettement les endroits où une erreur telle que tu décris pourrait se produire. (S'il n'y a qu'une classe dérivée à partir d'une base totalement abstraite, il n'y a pas besoin d'appeler le parent. Et s'il n'y en a que deux, la deuxième ne peut pas trop se tromper en ce qui concerne le parent à appeler.)
Du coup, j'ai cherché une solution à base de map et de pointeurs de fonction : La classe de base va gérer une map associant std::string (supposé être un type de message) et pointeurs sur fonctions membres.
C'est faisable, mais c'est lourd. Et ça ne vaut que rarement la peine.
Une des premières bibliothèques de fenêtrage, XViews, utlisait quelque chose de ce genre. On s'inscrivait pour les types de message intéressant, en passant le pointeur this et l'adresse d'une fonction membre. La solution que je viens de décrire me semble plus robuste et plus simple à mettre en oeuvre.
La méthode traite_message, non virtuelle, va utiliser cette map pour appeler la bonne méthode.
------------------------- fin code source --------------------------
Cette méthode semble fonctionner, mais j'ai un doute, notamment à cause de la conversion que je dois faire aux lignes indiquée par des astérisques. Cette conversion de void (*Derived::)(std::string const &) à void (*Base::)( std::string const &) est-elle légale ?
Tu veux dire "void (Derived::*)( std::string const& )" à "void (Base::*)( std::string const& )". Ça marche. (Je l'ai vérifié quand j'ai rencontré XViews.) Mais à mon avis, c'est une invitation à l'erreur (comme la plupart des conversions).
Plus généralement, que pensez vous de ces deux manières de faire ? Avez vous une préférence entre les deux, voire pour une troisième méthode que je n'ai pas vue ?
Franchement, je préfère la double dispatch (qui s'apparente au modèle de visiteur) ; sinon, ta première solution n'est pas vraiment mauvause non plus.
-- James Kanze (GABI Software) email: Conseils en informatique orientée objet/ Beratung in objektorientierter Datenverarbeitung 9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas écrire base->traite_message(type_message,message), il faut écrire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des octets reçus, à un objet message de la bonne classe. Évidement, en laissant le tampon se débrouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif fabrique (Factory Design Pattern), pour sélectionner et instancier un message de la bonne classe, en fonction du contenu du tampon (this).
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas écrire
base->traite_message(type_message,message), il faut écrire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des
octets reçus, à un objet message de la bonne classe. Évidement, en
laissant le tampon se débrouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif
fabrique (Factory Design Pattern), pour sélectionner et instancier un
message de la bonne classe, en fonction du contenu du tampon (this).
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas écrire base->traite_message(type_message,message), il faut écrire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des octets reçus, à un objet message de la bonne classe. Évidement, en laissant le tampon se débrouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif fabrique (Factory Design Pattern), pour sélectionner et instancier un message de la bonne classe, en fonction du contenu du tampon (this).
Merci beaucoup à vous et à Pascal pour ces critiques utiles et ces pistes intéressantes que je vais étudier avec le plus grand soin ! Je constate avec plaisir qu'on peut toujours compter sur fclc++ pour ce genre de questions ! :-)
Alexis Guillaume.
Merci beaucoup à vous et à Pascal pour ces critiques utiles et ces
pistes intéressantes que je vais étudier avec le plus grand soin ! Je
constate avec plaisir qu'on peut toujours compter sur fclc++ pour ce
genre de questions ! :-)
Merci beaucoup à vous et à Pascal pour ces critiques utiles et ces pistes intéressantes que je vais étudier avec le plus grand soin ! Je constate avec plaisir qu'on peut toujours compter sur fclc++ pour ce genre de questions ! :-)
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas crire base->traite_message(type_message,message), il faut crire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des octets re us, un objet message de la bonne classe. videment, en laissant le tampon se d brouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif fabrique (Factory Design Pattern), pour s lectionner et instancier un message de la bonne classe, en fonction du contenu du tampon (this).
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas crire
base->traite_message(type_message,message), il faut crire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des
octets re us, un objet message de la bonne classe. videment, en
laissant le tampon se d brouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif
fabrique (Factory Design Pattern), pour s lectionner et instancier un
message de la bonne classe, en fonction du contenu du tampon (this).
Ah, parce que chaque type de message a sa propre fonction?
Alors tu te trompe, il ne faut pas crire base->traite_message(type_message,message), il faut crire simplement:
message->traite_toi_toi_meme(); // !!!!!
La seule question, c'est comment on passe d'un tampon contenant des octets re us, un objet message de la bonne classe. videment, en laissant le tampon se d brouiller:
Bien entendu, transforme_toi_en_message pourra utiliser un motif fabrique (Factory Design Pattern), pour s lectionner et instancier un message de la bonne classe, en fonction du contenu du tampon (this).