Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Appel de la bonne fonction dans une hiérarchie de classe

4 réponses
Avatar
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.

Exemple fictif :

Base Messages g=E9r=E9s: evenement, identification
^
|
D=E9riv=E9e1 Messages g=E9r=E9s: informations_joueurs, quitte
^
|
D=E9riv=E9e2 Messages g=E9r=E9s: d=E9placement, tir, ...

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.

-------------------------- code source -----------------------------
#include <iostream>
#include <map>
#include <string>

class Base {
protected:
typedef void (Base::*msg_handler_t)( std::string const &type );
// M=E9thode appel=E9e par les classes de la hi=E9rarchie pour indiquer
// qu'elles savent g=E9rer un certain type de message
void RegisterHandler( std::string const &type,
msg_handler_t handler )
{
m_type_to_handlers[ type ] =3D handler;
}
private:
typedef std::map<std::string, msg_handler_t> map_handlers_t;
map_handlers_t m_type_to_handlers;
void func_base( std::string const &type ) {
std::cerr << "Base::func_base\n";
}
public:
Base() {
RegisterHandler( "func_base", &Base::func_base );
}
void traite_message( std::string const &type ) {
map_handlers_t::iterator it =3D m_type_to_handlers.find( type );
if (it !=3D m_type_to_handlers.end()) {
((*this).*(it->second))( type );
} else {
std::cerr << type << " not found.\n";
}
}
virtual ~Base() {}
};

class Derived1 : public Base {
void func_derived( std::string const &type ) {
std::cerr << "Derived1::func_derived\n";
}
public:
Derived1() {
// ****
RegisterHandler("func_derived",
(msg_handler_t)&Derived1::func_derived);
}
virtual ~Derived1() {}
};

class Derived2 : public Derived1 {
void func_derived2( std::string const &type ) {
std::cerr << "Derived2::func_derived2\n";
}
public:
Derived2() {
// *****
RegisterHandler("func_derived2",
(msg_handler_t)&Derived2::func_derived2);
}
virtual ~Derived2() {}
};

int main() {

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.

4 réponses

Avatar
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.



Exemple fictif :



Base Messages gérés: evenement, identification
^
|
Dérivée1 Messages gérés: informations_joueurs, quitte
^
|
Dérivée2 Messages gérés: déplacement, tir, ...



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 :

class MessageHandler ;

class Message
{
public:
virtual ~Message() {}
virtual void dispatch( MessageHandler* handler ) const
= 0 ;
} ;

class MessageA : public Message
{
public:
virtual void dispatch( MessageHandler* handler )
const ;
// ...
} ;
class MessageB : public Message { ... } ;
class MessageC : public Message { ... } ;

class MessageHandler
{
public:
virtual void notificationA( MessageA const* message ) =
0 ;
virtual void notificationB( MessageB const* message ) =
0 ;
virtual void notificationC( MessageC const* message ) =
0 ;
} ;

void
MessageA::dispatch(
MessageHandler* handler ) const
{
handler->notificationA( message ) ;
}

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.



-------------------------- code source -----------------------------


[coupé...]
------------------------- 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
Avatar
pjb
Alexis Guillaume writes:
[...]
Base *base = ....;
base->traite_message( type_message, message ) // (a)

Et ce en fonction du type de message.



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:

abstract_message* message=tampon->transforme_toi_en_message();

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).


--
__Pascal Bourguignon__ http://www.informatimago.com/
-----BEGIN GEEK CODE BLOCK-----
Version: 3.12
GCS d? s++:++ a+ C+++ UL++++ P--- L+++ E+++ W++ N+++ o-- K- w---
O- M++ V PS PE++ Y++ PGP t+ 5+ X++ R !tv b+++ DI++++ D++
G e+++ h+ r-- z?
------END GEEK CODE BLOCK------
Avatar
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 ! :-)

Alexis Guillaume.
Avatar
Alp Mestan
On 3 oct, 21:07, (Pascal J. Bourguignon) wrote:
Alexis Guillaume writes:
> [...]
> Base *base = ....;
> base->traite_message( type_message, message ) // (a)

> Et ce en fonction du type de message.

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:

abstract_message* message=tampon->transforme_toi_en_message();

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).

--
__Pascal Bourguignon__ http://www.informatimago.com/
-----BEGIN GEEK CODE BLOCK-----
Version: 3.12
GCS d? s++:++ a+ C+++ UL++++ P--- L+++ E+++ W++ N+++ o-- K- w---
O- M++ V PS PE++ Y++ PGP t+ 5+ X++ R !tv b+++ DI++++ D++
G e+++ h+ r-- z?
------END GEEK CODE BLOCK------



Ca rejoint ce qu'a dit James à la fin de son message : effectivement
il semble judicieux d'appliquer un visitor à tes messages.