OVH Cloud OVH Cloud

quel est le problème de type checking avec les callbacks ?

10 réponses
Avatar
meow
Hello,

je me mets =E0 Qt, et je suis actuellement sur leur m=E9canisme de
passage de messages (slots et signals). Dans la documentation en guise
d'intro il y a l'assertion :

"Callbacks have two fundamental flaws. Firstly they are not type
safe. We can never be certain that the processing function will call
the callback with the correct arguments."

Que je ne comprends pas. J'imagine qu'il s'agit encore d'une histoire
de compilation dynamique versus statique mais =E7a ne m'avance pas. Je
m'explique : pour moi un callback c'est juste un pointeur de fonction
(appelons le p) , et un pointeur de fonction est typ=E9 (on sait ce
qu'il renvoit et ce qu'il prend comme arguments). de meme, lorsque je
vais vouloir affubler une valeur =E0 p, j'aurais le type de p et celui
de la fonction f vers laquelle je veux pointer... Donc moyen de
d=E9tecter s'il y a un probl=E8me de type... Enfin, lorsque je vais
utiliser p (donc f), j'ai le type de p (qui est celui de f) et donc
encore moyen de v=E9rifier tous les types... Bref, je ne vois pas bien =E0
quel moment il peut y avoir un probl=E8me ?

10 réponses

Avatar
Fabien LE LEZ
On 1 Feb 2007 08:03:17 -0800, "meow" :

Je m'explique : pour moi un callback c'est juste un pointeur de fonction


Souvent, ça ne suffit pas. Il faut aussi un pointeur vers des données,
pour que la fonction callback sache quelles données utiliser (i.e.
sache quoi faire). Et comme la fonction principale ne connaît rien de
tes structures de données, le pointeur en question est souvent un
void*.

Avatar
espie
In article ,
meow wrote:


je me mets à Qt, et je suis actuellement sur leur mécanisme de
passage de messages (slots et signals). Dans la documentation en guise
d'intro il y a l'assertion :

"Callbacks have two fundamental flaws. Firstly they are not type
safe. We can never be certain that the processing function will call
the callback with the correct arguments."

Que je ne comprends pas. J'imagine qu'il s'agit encore d'une histoire
de compilation dynamique versus statique mais ça ne m'avance pas. Je
m'explique : pour moi un callback c'est juste un pointeur de fonction
(appelons le p) , et un pointeur de fonction est typé (on sait ce
qu'il renvoit et ce qu'il prend comme arguments). de meme, lorsque je
vais vouloir affubler une valeur à p, j'aurais le type de p et celui
de la fonction f vers laquelle je veux pointer... Donc moyen de
détecter s'il y a un problème de type... Enfin, lorsque je vais
utiliser p (donc f), j'ai le type de p (qui est celui de f) et donc
encore moyen de vérifier tous les types... Bref, je ne vois pas bien à
quel moment il peut y avoir un problème ?


Pour comprendre le probleme, il te faut analyser un peu plus. Dans
l'utilisation d'un callback, il y a deux bouts de logiciel:
- le widget
- l'application utilisateur.

Le probleme ici, c'est que c'est le widget qui est responsable de la
declaration du callback, et que c'est l'application qui va s'en servir.

En C, ca se termine pratiquement toujours par un
T (*callback)(T1 p1, T2 p2, ... , void *user_data);

Le probleme ici, c'est le user-data qui permet a l'application de retrouver
ce qu'elle veut comme donnees personnelles liees a l'utilisation du gadget,
et qu'on ne peut pas trop typer. On peut s'en sortir avec une hierarchie
de classes, mais ca impose d'avoir les user-data qui soient derivees d'une
classe specifique a l'execution.

Au moment ou l'interface de qt a ete concue, il n'y avait pas de mecanisme
en usage repandu dans le monde C++ qui permettait de faire ce que le
mecanisme de signal/slot de qt sait faire. Maintenant, si ma memoire est
bonne, on resoudrait ce probleme avec les bons templates et les bons
objets fonctionnels. Mais ce sont des techniques qui n'etaient pas vraiment
au point a l'epoque.

Comme tu le notes egalement, il y a aussi le cote dynamique de la chose,
de pouvoir accrocher slots et signaux de facon a peu pres arbitraire,
qui n'est pas si facile que ca a faire en C++ pur.


En fait, j'ai relu un bout d'un bouquin de smalltalk recemment, et je
vois que toute cette histoire de signaux et slots est tres fortement
inspiree des connexions utilisees dans ce dernier...

Avatar
James Kanze
Fabien LE LEZ wrote:
On 1 Feb 2007 08:03:17 -0800, "meow" :

Je m'explique : pour moi un callback c'est juste un pointeur de fonction


Souvent, ça ne suffit pas. Il faut aussi un pointeur vers des données,
pour que la fonction callback sache quelles données utiliser (i.e.
sache quoi faire). Et comme la fonction principale ne connaît rien de
tes structures de données, le pointeur en question est souvent un
void*.


Pas en C++. Les callback dont il est question ici ne sont rien
d'autre qu'une instance de l'observer pattern. Le callback même
est un EventListener, ou l'Observer, selon la terminologie
utilisée, et en dérive. Il n'y a aucune raison de sortir du
langage (comme fait Qt) pour avoir du type safety. (Pour une
autre solution, voir boost::signals, qui se base sur
boost::function, plutôt que d'une classe abstraite.)

--
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
James Kanze
Marc Espie wrote:
In article ,
meow wrote:

je me mets à Qt, et je suis actuellement sur leur mécanisme de
passage de messages (slots et signals). Dans la documentation en guise
d'intro il y a l'assertion :

"Callbacks have two fundamental flaws. Firstly they are not type
safe. We can never be certain that the processing function will call
the callback with the correct arguments."

Que je ne comprends pas. J'imagine qu'il s'agit encore d'une histoire
de compilation dynamique versus statique mais ça ne m'avance pas. Je
m'explique : pour moi un callback c'est juste un pointeur de fonction
(appelons le p) , et un pointeur de fonction est typé (on sait ce
qu'il renvoit et ce qu'il prend comme arguments). de meme, lorsque je
vais vouloir affubler une valeur à p, j'aurais le type de p et celui
de la fonction f vers laquelle je veux pointer... Donc moyen de
détecter s'il y a un problème de type... Enfin, lorsque je vais
utiliser p (donc f), j'ai le type de p (qui est celui de f) et donc
encore moyen de vérifier tous les types... Bref, je ne vois pas bien à
quel moment il peut y avoir un problème ?


Pour comprendre le probleme, il te faut analyser un peu plus. Dans
l'utilisation d'un callback, il y a deux bouts de logiciel:
- le widget
- l'application utilisateur.


C'est l'inversion de l'appel classique.

Le probleme ici, c'est que c'est le widget qui est responsable de la
declaration du callback, et que c'est l'application qui va s'en servir.

En C, ca se termine pratiquement toujours par un
T (*callback)(T1 p1, T2 p2, ... , void *user_data);


En C, oui. Mais nous sommes en C++, et personne qui ne connaisse
le C++ ferait comme ça.

Le probleme ici, c'est le user-data qui permet a l'application de retrouv er
ce qu'elle veut comme donnees personnelles liees a l'utilisation du gadge t,
et qu'on ne peut pas trop typer. On peut s'en sortir avec une hierarchie
de classes, mais ca impose d'avoir les user-data qui soient derivees d'une
classe specifique a l'execution.

Au moment ou l'interface de qt a ete concue, il n'y avait pas de mecanisme
en usage repandu dans le monde C++ qui permettait de faire ce que le
mecanisme de signal/slot de qt sait faire.


Tu veux dire que l'interface de Qt date d'avant les fonctions
virtuelles et des classes abstraites ? Je ne crois pas qu'elle
soit aussi ancienne.

Maintenant, si ma memoire est
bonne, on resoudrait ce probleme avec les bons templates et les bons
objets fonctionnels. Mais ce sont des techniques qui n'etaient pas vraime nt
au point a l'epoque.


Le modèle observeur l'était, et était même assez répandu (même
si on n'avait pas encore entendu parler des modèles de
conception à l'époque) au moins à l'époque où j'apprenais le
C++, c-à-d fin des 1980's, début des 1990's. Même aujourd'hui,
j'aurais tendance à l'utiliser plutôt que des templates, au
moins dans le cas des callback d'une GUI.

Comme tu le notes egalement, il y a aussi le cote dynamique de la chose,
de pouvoir accrocher slots et signaux de facon a peu pres arbitraire,
qui n'est pas si facile que ca a faire en C++ pur.


Tu plaisantes, non ? Même en Java, c'est facile (et c'est comme
ça qu'ont fonctionné toutes les versions de AWT et de Swing).

En C++, tu as une classe abstraite de base, et une collection de
pointeurs à des instances de la classe. Pour générer un signal,
tu itères sur la collection, en appelant la fonction concernée
sur le pointeur. Il me faudrait peut-être un quart d'heure pour
implémenter le tout, aujourd'hui. (Avant, la seule chose qui
manquait, c'était une collection standard dynamique, pour garder
les pointeurs.)

--
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
espie
In article ,
James Kanze wrote:
Comme tu le notes egalement, il y a aussi le cote dynamique de la chose,
de pouvoir accrocher slots et signaux de facon a peu pres arbitraire,
qui n'est pas si facile que ca a faire en C++ pur.


Tu plaisantes, non ? Même en Java, c'est facile (et c'est comme
ça qu'ont fonctionné toutes les versions de AWT et de Swing).


Non, je ne plaisante pas. Je ne me souviens plus du tout des details,
parce que j'ai lu ca il y a au moins deux ans, et je ne sais plus ou,
donc pour me rafraichir la memoire, ca va etre dur, mais il y a
deux/trois particularites des signaux/slots de qt qui ne
rentrent pas juste dans le modele `classique' dont tu parles, et qui
n'etaient reellement pas au point dans les compilo C++ existants au
moment ou ils ont concu la chose...


Avatar
Laurent Deniau
James Kanze wrote:
Marc Espie wrote:
Le probleme ici, c'est que c'est le widget qui est responsable de la
declaration du callback, et que c'est l'application qui va s'en servir.

En C, ca se termine pratiquement toujours par un
T (*callback)(T1 p1, T2 p2, ... , void *user_data);


En C, oui. Mais nous sommes en C++, et personne qui ne connaisse
le C++ ferait comme ça.


Meme en C je ne ferais pas comme ca. Il est plus simple de creer a la
main une fermeture. Au moins callback et user_data seront consistant.

a+, ld.


Avatar
Fabien LE LEZ
On 2 Feb 2007 02:35:01 -0800, "James Kanze" :

En C, ca se termine pratiquement toujours par un
T (*callback)(T1 p1, T2 p2, ... , void *user_data);


En C, oui. Mais nous sommes en C++, et personne qui ne connaisse
le C++ ferait comme ça.


Sauf quand c'est imposé par une bibliothèque prévue pour le C
(l'API Win32 par exemple).


Avatar
James Kanze
Fabien LE LEZ wrote:
On 2 Feb 2007 02:35:01 -0800, "James Kanze" :

En C, ca se termine pratiquement toujours par un
T (*callback)(T1 p1, T2 p2, ... , void *user_data);


En C, oui. Mais nous sommes en C++, et personne qui ne connaisse
le C++ ferait comme ça.


Sauf quand c'est imposé par une bibliothèque prévue pour le C
(l'API Win32 par exemple).


Mais tu ne le fais pas dans des classes qui fournissent la
façade de l'API. Et tu n'utilises pas d'API propriétaire
dans l'application sauf à travers une façade, non ? (Même
si Posix n'est pas strictement parlant propriétaire, je ne
m'en sers pas dans l'application.)

--
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
meow
Alors pour commencer, en vous lisant je prends conscience que le terme
meme de "callback" est équivoque. Je l'avais moi aussi compris à
l'"ancienne" : pointeur de fonction. Je n'ai jamais rien lu sur le
sujet, donc, en outre, je n'ai aucune idée sur la manière dont ces
techniques doivent etre employées. J'avais bien imaginé le coup du
void*, mais j'imaginais qu'avec le polymorphisme, en passant à C++ on
pouvait se permettre une dérivation propre et une gestion correcte des
types à la compilation...
Le pattern Observer (ou listener, si je ne me trompe pas c'est son
autre nom, non ?), je commence vaguement à le connaitre, oui, et à
bien y réfléchir, ça me semble etre une "implémentation" "propre" de
ce que j'esquissais une phrase plus haut.

Bref... Il semblerait que la vraie question soit : qu'est ce que le
"webmaster" de Qt entend par *callback* ? Parce que quand je lis :

"The processing function then calls the callback when appropriate.
Callbacks have two fundamental flaws. Firstly they are not type safe.
We can never be certain that the processing function will call the
callback with the correct arguments. Secondly the callback is strongly
coupled to the processing function since the processing function must
know which callback to call."

Je me dis que le pattern Observer répond justement aux deux
problématiques évoquées. D'ailleurs que fait le moc ? Je ne suis pas
allé regarder le code généré mais je ne serais pas étonné d'y v oir une
implémentation de ce pattern...

(PS: les extraits de texte sont issus de la page http://
doc.trolltech.com/3.3/signalsandslots.html)

@Marc> Ne fais pas de spéléo pour ça, mais si tu trouves l'exemple je
serai intéressé d'y jeter un oeil.
Avatar
Mathias Gaunard

(Pour une
autre solution, voir boost::signals, qui se base sur
boost::function, plutôt que d'une classe abstraite.)


boost::function utilise aussi une classe abstraite.

A priori (je n'ai pas vérifié, mais je pense que ça fonctionne ainsi)
une implémentation naïve (une meilleure implémentation traiterait à part
les cas des pointeurs de fonction et des pointeurs de fonctions membres
car leur taille est fixe, d'où la possibilité d'éviter l'allocation
dynamique) donne

template<typename R>
class function0
{
struct base
{
virtual R operator() = 0;
virtual base* clone() = 0;
virtual ~base() = 0;
};

template<typename F>
struct final : public base
{
final(const F& f) : f_(f)
{
}

R operator()
{
return f_();
}

base* clone()
{
return new final<F>(f_);
}

~final()
{
}

F f_;
}

public:
template<typename F>
function0(const F& f) : p_(new final<F>(f))
{
}

function0(const function0& f) : p_(f.p_->clone())
{
}

template<typename F>
function0& operator=(const F& f)
{
delete p_;
p_ = new final<F>(f);

return *this;
}

function0& operator=(const function0& f)
{
delete p_;
p_ = f.p_->clone();

return *this;
}

R operator()
{
return (*p_)();
}

~function0()
{
delete p_;
}

private:
base* p_;

};

Donc en fait, l'idée reste la même.
C'est juste qu'on enveloppe ça dans un objet.

Bien sûr ça c'est le cas sans argument. Ensuite il faut faire le cas
avec un, puis deux, puis n arguments.