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

objets fonction : pourquoi ?

11 réponses
Avatar
meow
Je m'interroge simplement sur les raisons qui ont pr=E9sid=E9es =E0 la
cr=E9ation des "function objects" et sur la bonne mani=E8re de les
utiliser. Etait-t'il n=E9cessaire d'inventer cette "nouvelle tentacule"
? Y-a t'il des exemples dans lesquelles il n'est pas possible de passer
outre par un design ? Par exemple, si je ne m'abuse, l'emploi des
objets fonction dans les algorithmes sort() de la STL revient ni plus
ni moins =E0 l'utilisation d'un pattern de type strategie.

10 réponses

1 2
Avatar
Loïc Joly
Je m'interroge simplement sur les raisons qui ont présidées à la
création des "function objects" et sur la bonne manière de les
utiliser. Etait-t'il nécessaire d'inventer cette "nouvelle tentacule"
? Y-a t'il des exemples dans lesquelles il n'est pas possible de passer
outre par un design ? Par exemple, si je ne m'abuse, l'emploi des
objets fonction dans les algorithmes sort() de la STL revient ni plus
ni moins à l'utilisation d'un pattern de type strategie.


Je dirais plutôt que les function objets sont une des méthode permettant
d'implémenter le pattern stratégie. D'autres méthodes incluent les
classes avec une fonction virtuelle, les pointeurs de fonction...

Avantages des function objets :
- Dans certains cas, suivant la façon dont ils sont faits, permettent
une résolution à la compilation et un inlining complet, ce qui se
traduit très positivement en terme de perfs.
- Il sont légers à mettre en place, en particulier, il permettent de
réutiliser aisément du code existant.

--
Loïc

Avatar
kanze
meow wrote:

Je m'interroge simplement sur les raisons qui ont présidées à
la création des "function objects" et sur la bonne manière de
les utiliser. Etait-t'il nécessaire d'inventer cette "nouvelle
tentacule" ? Y-a t'il des exemples dans lesquelles il n'est
pas possible de passer outre par un design ? Par exemple, si
je ne m'abuse, l'emploi des objets fonction dans les
algorithmes sort() de la STL revient ni plus ni moins à
l'utilisation d'un pattern de type strategie.


L'intérêt principal, par rapport à une fonction (ou un pointeur
à une fonction), c'est que l'objet peut contenir de l'état. Un
bon exemple : je veux chercher (find/find_if) le premier
caractère non-espace dans une chaîne de caractères. Je pourrais
bien écrire une fonction isNotEspace, et le passer à find_if,
mais elle risque de ne pas être très performant -- il faudrait
par exemple qu'elle appelle std::use_facet< std::ctype<char> >()
chaque fois qu'on l'appelle -- et en plus, elle ne pourrait
travailler que sur un locale donné (probablement le locale
global). Tandis qu'avec un objet fonctionnel, j'écris :

class IsNotSpace
{
public:
explicit IsNotSpace(
std::locale const& l = std::locale() )
;
bool operator()( char ch ) const
{
return ! myCType->is( std::CType::space, ch ) ;
}

private:
typedef std::ctype< char >
CType ;
CType const* myCType ;
} ;

IsNotSpace::IsNotSpace(
std::locale const& l )
: myCType( &std::use_locale< CType >( l ) )
{
}

et :
std::find_if( chaine.begin(), chaine.end(), IsNotSpace() ) ;

Seulement, si je veux, je peux préciser le locale.

Ensuite, c'est assez facile à le convertir en template :

template< bool testFor,
std::ctype_base::mask mask,
typename charT = char >
class Is
{
public:
typedef std::ctype< charT >
CType ;
explicit Is( std::locale const& l = std::locale() )
: myCType( &std::use_locale< CType >( l ) )
{
}
bool operator()( charT ch ) const
{
return myCType->is( mask, ch ) == testFor ;
}

private:
CType const* myCType ;
} ;

pour pouvoir écrire :
typedef std::ctype_base M ;

std::find_if( chaine.begin(), chaine.end(), Is< false, M::space >()
)
std::find_if( chaine.begin(), chaine.end(),
Is< true, M::alpha >( std::locale::classic() ) )

et ainsi de suite.

La vitesse d'exécution y gagne aussi, parce qu'avec l'objet
fonctionnel, c'est le type de l'objet (et non sa valeur, comme
c'est le cas d'un pointeur à une fonction ou à un objet avec une
fonction virtuelle) qui détermine quelle fonction sera appelée
-- c-à-d que ce le compilateur le sait, et qu'il peut optimiser
en conséquence (génération en ligne, par exemple).

Comme tu dis, c'est un peu comme le modèle stratégie. Sauf que
la résolution est au moment de la compilation, non au moment de
l'exécution. Tu perds donc en souplesse et en dynamisme, mais tu
gagne en facilité d'utilisation (pas besoin d'hériter d'une
classe de base donnée, etc.) et en sécurité (prédicats de type
vérifiés par le compilateur).

--
James Kanze GABI Software
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
Comme d'hab, merci pour ces explications, et merci aussi au passage
d'avoir attiré mon attention sur le mot clef "explicit". Cela étant,
pour une fois j'ai beau lire et relire vos messages j'ai du mal à
comprendre ! :/

la légereté, ça à la limite je comprends. L'inlining complet, là
déjà, je coinces... Je crts savoir ce qu'est l'inlining mais je ne
vois pas en quoi les objets fonction le permettent plus qu'un autre
modèle à base de fonction ou de classes dérivées.

En ce qui concerne le cas de figure énnoncé par Kanze avec la
fonction isNotEspace, je pars avec un gros handicap : je ne suis pas
sensibilisé au problème des locale, j'y ai jeté un oeil rapide mais
ça a l'air tout de meme un peu trop gros pour une compréhension en 5
minutes :) Bref, std::use_facet, Ctype et Cie, ça ne me parle pas
trop, du coup je vois pas le problème ! ;)

L'exemple concret, bien qu'un peu complexe par contre me permet déjà
mieux de tutoyer des éléments de réponse. moi, naivement j'aurai
passé le pattern à trouver en argument :

template< bool testFor,
typename charT = char >
class Is
{
public:
typedef std::ctype< charT >
CType ;
explicit Is( std::ctype_base::mask pattern,
std::locale const& l = std::locale() )
: myCType( &std::use_locale< CType >( l ) ),
mask(pattern)
{
}
bool operator()( charT ch ) const
{
return myCType->is( mask, ch ) == testFor ;
}

private:
CType const* myCType ;
std::ctype_base::mask mask;
} ;
}

...certainement par "peur" de voir le compilo possiblement générer
autant de classes que de caractères unicodes ;)
Et ce petit exemple de me faire me remarquer qu'à partir du moment où
l'objet fonction trimbale des données ça devient pas trop possible
d'inliner, non ? C'est ça le truc ? inline ssi pas de donnée dans
l'objet ?

Il y a aussi l'histoire du gain en sécurité qui reste obscure pour
moi :
(prédicats de type vérifiés par le compilateur).



Je remarques que j'utilise peut etre les objets fonctionnels un peu à
contre emploi ou de manière batarde à la "modèle stratégie"
(dérivation d'une classe abstraite pure template <class O>Neighborhood
avec "bool operator=(Oconst&,Oconst&) =0" puis dérivation en
NeighborhoodBySides, NeighborhoodByVertex,... etc) ...il faut que je
relise mon code pour approfondir la question et faire un choix, parce
qu'à priori on dirait bien que je ne fais rien d'autre ici qu'un
modèle dans lequel la fonction à fournir s'appelle operator= , mais
elle pourrait tout aussi bien s'appeller foo !

Avatar
kanze
meow wrote:

la légereté, ça à la limite je comprends. L'inlining complet,
là déjà, je coinces... Je crts savoir ce qu'est l'inlining
mais je ne vois pas en quoi les objets fonction le permettent
plus qu'un autre modèle à base de fonction ou de classes
dérivées.


Prenons un petit exemple simple, sans templates :

void
f( void (*pf)() )
{
pf() ;
}

Ici, pf est une variable. Sauf une analyse plus ou moins
approfondie, le compilateur ne sait pas quelle fonction sera
appelée, et donc, ne peut pas la générer en ligne. Maintenant,
avec objet fonctionnel :

struct F
{
void operator()() const
{
// faire quelque chose de simple...
}
} ;

void
f( F pf )
{
pf() ;
}

Ici, le compilateur voit bien quelle fonction serait appelée, et
peut la générer en ligne.

Maintenant, sans templates, comme ci-dessus, il n'y a pas
d'intérêt -- à quoi sert un paramètre qui ne contient pas de
données, et dont la fonction qu'on appelle n'est pas virtuelle,
et donce est toujours la même ? Mais si on considère le cas des
templates... ces deux fonctions sont en fait deux instantiations
de la même template :

template< typename F >
void
f( F pf )
{
pf() ;
}

Et on peut l'appeler soit avec l'adresse d'une fonction :

inline void
myFunc()
{
// ...
}

f( &myFunc ) ;

soit avec un objet fonctionnel :

struct F { /* comme ci-dessus */ } ;

f( F() ) ;

Dans le premier cas, le type de la fonction instantiée, c'est
void ( void(*)() ), c-à-d qu'on se trouve dans le même cas que
la première fonction ci-dessus ; dans le second, c'est avec
l'objet fonctionnel dont la fonction est connue du compilateur.

Il y a une contrapartie, évidemment. Si tu appelles f avec les
adresses de 100 fonctions différentes, c'est toujours la même
instantiation -- et tu n'as en fin de compte qu'une copie de f
dans ton code. Avec les objets fonctionnels, en revanche, tu as
autant d'instantiations et de copies qu'il y a de types
différents de l'objet.

En ce qui concerne le cas de figure énnoncé par Kanze avec la
fonction isNotEspace, je pars avec un gros handicap : je ne
suis pas sensibilisé au problème des locale, j'y ai jeté un
oeil rapide mais ça a l'air tout de meme un peu trop gros pour
une compréhension en 5 minutes :) Bref, std::use_facet, Ctype
et Cie, ça ne me parle pas trop, du coup je vois pas le
problème ! ;)


Malheureusement, même sans vouloir supporter plusieurs locales,
il faut bien faire quelque chose. Essaie une fois d'utiliser
isspace comme paramètre de find_if sur une chaîne -- si ça
compile, tu vas te rétrouver avec un comportement indéfini,
parce que le seul cas où il n'y a qu'un seul isspace (et donc,
que l'appel n'est pas ambigu), c'est si tu as inclu <ctype.h>
(ou <cctype>), et ni toi, ni un autre en-tête que tu as inclu,
n'as inclu <locale>. Et le isspace dans <ctype.h> ne prend pas
de char comme paramètre, mais un int avec une valeur dans
l'intervale 0...UCHAR_MAX. (Et grace aux conversions implicites
de C++, l'appel avec un char abouti, mais en général, avec un
paramètre qui n'est pas forcément dans l'intervale voulu.)

L'exemple concret, bien qu'un peu complexe par contre me
permet déjà mieux de tutoyer des éléments de réponse. moi,
naivement j'aurai passé le pattern à trouver en argument :

template< bool testFor,
typename charT = char >
class Is
{
public:
typedef std::ctype< charT >
CType ;
explicit Is( std::ctype_base::mask pattern,
std::locale const& l = std::locale() )
: myCType( &std::use_locale< CType >( l ) ),
mask(pattern)
{
}
bool operator()( charT ch ) const
{
return myCType->is( mask, ch ) == testFor ;
}

private:
CType const* myCType ;
std::ctype_base::mask mask;
} ;
}

...certainement par "peur" de voir le compilo possiblement générer
autant de classes que de caractères unicodes ;)


Ce n'est pas un « pattern », la masque. C'est une valeur d'enum,
avec un nombre assez faible de valeurs possibles : space, alpha,
digit, etc. (8 ou 9 en tout). Mais c'est vrai qu'ici, les deux
possibilités peuvent se discuter. L'intérêt de le passer comme
paramètre de la template, c'est principalement qu'à mon avis,
c'est plus clair comme ça -- ce n'est pas quelque chose de
dynamique, mais bien une constante qui définit en quelque sort
ce que fait la fonction. (Enfin, c'est mon point de vue.) Mais
les deux solutions se vaut, en quelque sort.

Seulement, on avait parler des optimisations que permet les
objets fonctionnels. Or, quand charT est char, ctype<char> a une
spécialisation où la fonction « is » a de fortes chances d'être
inline ... elle se résume à quelque chose du genre :
return (table[ ch ] & mask) != 0 ;
Ensuite, ma fonction, ci-dessus, est aussi inline (du fait
qu'elle est définie dans la définition de la classe). Du coup,
il y a une chance (si le compilateur n'est pas trop mauvais) que
dans l'instantiation de find_if, tout est inline -- on finit
avec quelque chose du genre :

while ( begin != end && (table[ *begin ] & mask) != 0 ) {
++ begin ;
}

Si mask n'est pas une constante, il y a une chance que le
compilateur ne le reconnaît pas comme une, et qu'il le rélit
de mémoire chaque fois dans la boucle. Et dans une boucle aussi
serrée, ça pourrait faire une différence.

Et ce petit exemple de me faire me remarquer qu'à partir du
moment où l'objet fonction trimbale des données ça devient pas
trop possible d'inliner, non ? C'est ça le truc ? inline ssi
pas de donnée dans l'objet ?


Pourquoi pas ? L'inline ne concerne le code dans la fonction.
Qu'il y ait des données ou non n'y change rien. Mais il y a des
chances qu'une donnée dans l'objet serait rélue de mémoire
chaque fois que la fonction est appelée (même si l'appel n'a pas
réelement lieu, à cause de l'inline). Tandis qu'un paramètre de
template, c'est une constante connue du compilateur.

Il y a aussi l'histoire du gain en sécurité qui reste obscure
pour moi :

(prédicats de type vérifiés par le compilateur).



C'est l'idée derrière le template. Ici, il ne joue pas, mais
dans d'autres cas, si. Ça n'a du rapport avec l'objet
fonctionnel que parce que l'objet fonctionnel est en général un
template.

Je remarques que j'utilise peut etre les objets fonctionnels
un peu à contre emploi ou de manière batarde à la "modèle
stratégie" (dérivation d'une classe abstraite pure template
<class O>Neighborhood avec "bool operator=(Oconst&,Oconst&)
=0" puis dérivation en NeighborhoodBySides,
NeighborhoodByVertex,... etc) ...il faut que je relise mon
code pour approfondir la question et faire un choix, parce
qu'à priori on dirait bien que je ne fais rien d'autre ici
qu'un modèle dans lequel la fonction à fournir s'appelle
operator= , mais elle pourrait tout aussi bien s'appeller foo
!


Comme j'ai dit : l'utilisation des objets fonctionnel comme
paramètre d'instantiation d'un template (et c'est leur
utilisation principale) fait à peu près ce que fait le modèle de
stratégie, mais au moment de la compilation -- ce n'est pas
aussi dynamique. (Mais c'est cette perte de dynamisme qui peut
mener à une meilleur sécurité dans certains cas -- il permet au
compilateur de vérifier d'avantage de choses.)

--
James Kanze GABI Software
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
Ici, le compilateur voit bien quelle fonction serait appelée, et
peut la générer en ligne.
Si je comprends bien , c'est le fait de passer d'une double

indirrection à une simple indirrection "qui sauve" ?

void h(){...};
f(void (*pf)()){...;pf();...} // ici on ne sait pas encore ce qui va
etre appellé
f(h); // on ne le sait que là

alors que:
struct F { void operator()(){...}; };
f(F pf){...;pf();...} // on le sait déjà et le compilo peut déjà
inliner

à quoi sert un paramètre qui ne contient pas de
données, et dont la fonction qu'on appelle n'est pas virtuelle,
et donc est toujours la même ?
Certes, et c'est pourquoi j'ai toujours considéré les objets

fonctions avec une partie données initialisée par un constructeur :
class IsInfTo{
private :
int compare;
public :
IsInfTo(int c):compare(c){}
bool operator(int to_be_compare){return(to_be_compare<compare);}
}
Avec le int possiblement transformé en T, paramètre template (ce qui
au passage me laisse toujours un drole d'arrière gout dans la mesure
où ma classe dépend alors de l'existence d'un operateur< dans la
classe T... Mais c'est un autre thread que j'ouvrirai certainement un
jour.).

Avec tes commentaires j'ai enfin compris pas mal de choses :
La "variable" compare peut etre passée en parametre template ou en
parametre de constructeur sans modifier le comportement du compilo en
inline. En effet, ça n'impacte pas vraiment sur le code, cela le
modifie simplement dans la modification d'une constante en une adresse
mémoire de variable. On retombe donc juste sur deux problèmes :
a. si l'operateur () de l'objet fonction est effectivement inliné,
la différrence entre les deux approches consiste dans l'ajout d'un
déréférencement
b. si l'operateur () de l'objet fonction n'est pas inliné, on
remplace un appel à des fonctions différentes par un
déréférencement de variable lors des appels. C'est à dire un code
plus long (mais moins que dans le cas b., contre un code plus court
mais plus lent).

Une question subsiste, pourquoi avoir redéfini () ? Est-ce que la
définition d'une fonction quelconque n'aurait pas suffi ? Pour
reprendre l'exemple du début :
struct F { F(); void toto(){...}; };
f(F pf){...;pf.toto();...} // on sait déjà quelle fonction toto,
d'où inline possible, non ?

J'ai bien un élément de réponse à ma question : par soucis de
clarté du code :

IsInfTo<int,7> isIntInfTo7;
isIntInfTo7(9);

est peut etre plus clair que
IsInfTo<int,7> is7;
is7.biggerThan(9);

Mais je ne suis pas super convaincu... :)

Avatar
Arnaud Meurgues
meow wrote:

Une question subsiste, pourquoi avoir redéfini () ? Est-ce que la
définition d'une fonction quelconque n'aurait pas suffi ?


Moi, je vois ça comme du pur sucre syntaxique. Ou peut-être un
édulcorant dans la mesure où l'on ne sait plus s'il s'agit d'une
fonction ou d'un objet lorsqu'on ne voit que l'appel : en lisant
f();
f est-il une fonction, un pointeur de fonction ou bien un objet
fonctionnel ?

Mais, à mon sens, faire une classe avec un opérateur () ou une classe
avec une fonction apply() me semble terriblement équivalent.

La seule différence que l'on peut voir est l'usage dans un template.
Quelque chose comme :

template <typename F>
int apply(F f)
{
return f();
}

peut prendre les deux types (fonction renvoyant int et objet fonctionnel
disposant d'un opérateur() renvoyant int). Ça donne ceci :

#include <iostream>

template <typename F>
int apply(F f)
{
return f();
}

int function() {
return 1;
}

struct Functionnal {
int operator() () { return 2;}
};

int main() {
int (*f1)() = &function;
Functionnal f2;
std::cout << "apply(f1) = " << apply(f1) << "n";
std::cout << "apply(f2) = " << apply(f2) << "n";
}

--
Arnaud

Avatar
meow
std::cout << "apply(f1) = " << apply(f1) << "n";
O_o'

Euh... On est pas obligé d'instancier les templates ?
apply<int (*)()>(f1) ????

Avatar
nmartin
std::cout << "apply(f1) = " << apply(f1) << "n";


O_o'
Euh... On est pas obligé d'instancier les templates ?
apply<int (*)()>(f1) ????

bonjour,

sur les fonctions template le compilateur effectue une résolution
automatique du type et il n'est, en general, pas necessaire de specifier
les parametres.

nicolas.


Avatar
Arnaud Meurgues
meow wrote:

std::cout << "apply(f1) = " << apply(f1) << "n";


Euh... On est pas obligé d'instancier les templates ?
apply<int (*)()>(f1) ????


Non. Il infère le type du template à instancier en fonction du type du
paramètre.

Accessoirement, ça compile et tourne sur Visual C++ 8 (2005).

--
Arnaud


Avatar
James Kanze
meow wrote:
Ici, le compilateur voit bien quelle fonction serait appelée, et
peut la générer en ligne.
Si je comprends bien , c'est le fait de passer d'une double

indirrection à une simple indirrection "qui sauve" ?


Pas vraiment. C'est le fait que dans un objet fonctionnel, c'est
le type qui détermine la fonction appelé, tandis que si le type
est un pointeur à une fonction, c'est la valeur (c-à-d à
l'exécution) qui détermine quelle fonction à appeler.

void h(){...};
f(void (*pf)()){...;pf();...} // ici on ne sait pas encore ce qui va
etre appellé
f(h); // on ne le sait que là


Par exemple. Et il faut que le compilateur suit le paramètre,
pour détecter que c'est bien une constante qui a été passée. Et
qu'il régarde tout le programme, pour vérifier que la fonction
n'est pas appelée ailleurs avec un paramètre différent. Dans le
cas d'une fonction globale, il y a très, très peu de
compilateurs qui en sont capables.

alors que:
struct F { void operator()(){...}; };
f(F pf){...;pf();...} // on le sait déjà et le compilo peut déjà
inliner


Oui, parce que quelque soit la valeur de pf, la fonction reste
la même. La fonction dépend ici du type, non de la valeur.

Rends la fonction dans F virtuelle, et on se rétrouve plus ou
moins dans le premier scénario.

à quoi sert un paramètre qui ne contient pas de
données, et dont la fonction qu'on appelle n'est pas virtuelle,
et donc est toujours la même ?



La question était un peu construite en vue de mener aux
templates. Au moins de contenir des données qui en modifie le
comportement, l'utilisation d'un objet fonctionnel n'a pas de
sens sans templates.

Certes, et c'est pourquoi j'ai toujours considéré les objets
fonctions avec une partie données initialisée par un constructeur :
class IsInfTo{
private :
int compare;
public :
IsInfTo(int c):compare(c){}
bool operator(int to_be_compare){return(to_be_compare<compare);}
}
Avec le int possiblement transformé en T, paramètre template (ce qui
au passage me laisse toujours un drole d'arrière gout dans la mesure
où ma classe dépend alors de l'existence d'un operateur< dans la
classe T... Mais c'est un autre thread que j'ouvrirai certainement un
jour.).


Avec tes commentaires j'ai enfin compris pas mal de choses :
La "variable" compare peut etre passée en parametre template ou en
parametre de constructeur sans modifier le comportement du compilo en
inline. En effet, ça n'impacte pas vraiment sur le code, cela le
modifie simplement dans la modification d'une constante en une adresse
mémoire de variable. On retombe donc juste sur deux problèmes :
a. si l'operateur () de l'objet fonction est effectivement inliné,
la différrence entre les deux approches consiste dans l'ajout d'un
déréférencement
b. si l'operateur () de l'objet fonction n'est pas inliné, on
remplace un appel à des fonctions différentes par un
déréférencement de variable lors des appels. C'est à dire un code
plus long (mais moins que dans le cas b., contre un code plus court
mais plus lent).


Je me démande si tu n'es pas en train de mettre trop dans mon
exemple. Je l'ai présenté dans l'unique but de mener aux
templates. Dans un template, l'intérêt, c'est que chaque type
d'instantiation donne une fonction différente. Qui appelle
toujours la même fonction, connue du compilateur, et donc, que
le compilateur peut mettre inline. Tandis qu'avec un pointeur à
une fonction, il n'y a qu'un seul type, donc, une seule
instantiation du template, ou de différentes fonctions peuvent
être appelées. Ce qui, évidemment, rend la génération inline
beaucoup plus difficile, sinon impossible.

Une question subsiste, pourquoi avoir redéfini () ? Est-ce que la
définition d'une fonction quelconque n'aurait pas suffi ?


Là aussi, on vise les templates. Si le template appelle pf(), le
type de pf peut être soit un objet fonctionnel, soit un pointeur
à une fonction. Si tu as déjà une fonction qui fait l'affaire,
tu peux en passer l'adresse, sans avoir besoin de définir une
nouvelle classe.

Sinon, effectivement, il est prèsque toujours préférable à
donner un nom à la fonction. Encore que... Il y a des fois que
le rôle de l'objet, c'est bien celui d'une fonction. Pense, par
exemple, à la fonction classique pour calculer un integral :

double integrate( double (*f)( double ),
double lowerBound,
double upperBound ) ;

Il s'agit bien d'une fonction (au sens mathématique, en première
ligne) qu'on passe. En C++ d'aujourd'hui, on écrira ou bien :

class Function
{
public :
virtual ~Function() {}
virtual double operator()( double arg ) const = 0 ;
} ;
double integrate( Function const& f,
double lowerBound,
double upperBound ) ;

ou

template< typename Function >
double integrate( Function f,
double lowerBound,
double upperBound ) ;

La première forme est l'écriture classique, pré-template.
Aujourd'hui, je crois qu'on privilègerait la deuxième, même si
ça impose à mettre toute l'implémentation de la fonction dans
l'en-tête, et donne autant de copies de la fonction qu'il y a de
types de paramètres différents. Mais dans les deux cas,
l'utilisation d'un operator()() me semble tout à fait indiquée.

Y compris les cas où l'objet contient des données -- son rôle
dans le programme, c'est d'être appelé.

Pour
reprendre l'exemple du début :
struct F { F(); void toto(){...}; };
f(F pf){...;pf.toto();...} // on sait déjà quelle fonction toto,
d'où inline possible, non ?


J'ai bien un élément de réponse à ma question : par soucis de
clarté du code :


IsInfTo<int,7> isIntInfTo7;
isIntInfTo7(9);

est peut etre plus clair que
IsInfTo<int,7> is7;
is7.biggerThan(9);


Mais je ne suis pas super convaincu... :)


Si le nom de la classe est isInfTo, ça suggère bien une
prédicate, ce qui est une fonction. Quelque chose comme
IsInfTo<int,7>().isBiggerThan(9) est plutôt rédondant. En
revanche, il faut bien que la rôle de l'objet dans un programme
soit vraiement celle d'une fonction, et que les noms attendus
des variables l'en reflètent. Pour les objets dit fonctionnel,
c'est le cas, d'office -- ils n'ont pas d'autre comportement
(ni, normalement, d'état variable -- s'il y a état, c'est pour
paramètrisé la fonction appelée).

Et quel nom donner en général ? Prenons le cas d'« integrate »,
ci-dessus. Quel nom est-ce que tu pourrais raisonablement donner
à la fonction membre ? Où dans les prédicates de la bibliothèque
standard -- c'est une prédicate, c'est donc isSomething(), mais
quoi ? Simplement « isWhateverTheUserWants() » n'est pas
vraiment un nom qui me plaît. (Le problème ici, avec les
prédicates, c'est que la convention doit convenir des deux
côtés. Dans prèsque tous les cas concrets, il y a bien un nom
qui conviendra à la prédicate même. Mais ça serait jamais le
même nom, et il faut le même nom pour le template ou la classe
d'interface.)

Mais je crois qu'actuellement, il y a bien une tendance à abuser
de cette possibilité -- d'utiliser l'operator()() trop souvent,
dans les cas qui ne lui convient pas. C'est une mode (qui
passera, sans doute).

--
James Kanze
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


1 2