Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
callable [2016/12/18 23:18] gbdivers |
callable [2019/03/10 11:09] (Version actuelle) alavida [Les fonctions génériques] |
||
---|---|---|---|
Ligne 3: | Ligne 3: | ||
- | ====== [Aller plus loin] Les fonctions comme données ====== | + | ====== [Aller plus loin] Manipuler une fonction dans une variable ====== |
- | En première approche, une fonction peut être vu comme une boîte noire, qui peut prendre des informations (les arguments), réaliser une tâche et peut retourner un résultat. Les informations en entrée et sortie seront généralement des données plus ou moins complexes : des entiers, des réels, des chaînes, etc. | + | En première approche, une fonction peut être vue comme une boîte noire, qui peut prendre des informations (les arguments), réaliser une tâche et peut retourner un résultat. Les informations en entrée et sortie seront généralement des données plus ou moins complexes : des entiers, des réels, des chaînes, etc. |
- | En fait, les fonctions elle-même sont des informations. Elles peuvent être manipuler comme n'importe quelle donnée, être conservées dans des variables ou être utilisées comme arguments de fonctions. Cela permet de modifier le comportement du programme par le programme lui-même. | + | En fait, les fonctions elles-mêmes sont des informations. Elles peuvent être manipulées comme n'importe quelle donnée, être conservées dans des variables ou être utilisées comme arguments de fonctions. Cela permet de modifier le comportement du programme par le programme lui-même. |
- | Un exemple concret d'une telle utilisation. Imaginez que vous travaillez sur un jeu vidéo et vous souhaitez donner la possibilité au joueur de pouvoir changer les actions selon les touches du clavier. Par exemple que la touche A puisse servir a sauter, a avancer ou a tirer, selon le choix du joueur. Une solution est de créer des fonctions ''sauter'', ''avancer'' et ''tirer'' et d'avoir une variable ''touche_A''. Il suffit ensuite de sélectionner quelle fonction sera affectée à la variable ''touche_A'' et a chaque fois que le joueur appuie sur cette touche, le jeu exécute l'action correspondante. | + | Un exemple concret d'une telle utilisation. Imaginez que vous travaillez sur un jeu vidéo et vous souhaitez donner la possibilité au joueur de pouvoir changer les actions selon les touches du clavier. Par exemple que la touche A puisse servir à sauter, à avancer ou à tirer, selon le choix du joueur. Une solution est de créer des fonctions ''sauter'', ''avancer'' et ''tirer'' et d'avoir une variable ''touche_A''. Il suffit ensuite de sélectionner la fonction à affecter à la variable ''touche_A'' et à chaque fois que le joueur appuie sur cette touche, le jeu exécute l'action correspondante. |
- | Vous pourrez voir aussi le terme //callback// (fonction de rappel) pour désigner cette approche. | + | Vous pourrez aussi voir le terme //callback// (fonction de rappel) pour désigner cette approche. |
Ligne 22: | Ligne 22: | ||
</code> | </code> | ||
- | En fait, les fonctions ne sont pas les seules syntaxes en C++ qui peuvent s'appeler de cette façon. L'ensemble de syntaxes qui peuvent être appelées de cette façon sont appelés des //callables//, ce qui peut être traduit par "appelables". Ce terme n'est pas vraiment correct en français, le terme anglais est probablement préférable. | + | En fait, les fonctions ne sont pas les seules syntaxes en C++ qui peuvent s'appeler de cette façon. L'ensemble des syntaxes qui peuvent être appelées de cette façon est appelé //callable//, ce qui peut être traduit par "appelable". Ce terme n'est pas vraiment correct en français, le terme anglais est probablement préférable. |
Vous avez déjà rencontré des //callables// qui ne sont pas des fonctions : les foncteurs (aussi appelé fonction-objet) de la bibliothèque standard, comme ''std::less'' ("plus petit que") ou ''std::plus''. | Vous avez déjà rencontré des //callables// qui ne sont pas des fonctions : les foncteurs (aussi appelé fonction-objet) de la bibliothèque standard, comme ''std::less'' ("plus petit que") ou ''std::plus''. | ||
<code cpp> | <code cpp> | ||
- | std::less<int>(12, 34); // est equivalent a (12 < 34) | + | std::less<int>{}(12, 34); // est équivalent à (12 < 34) |
- | std::plus<float>(1.2, 3.4); // est equivalent a (1.2 + 3.4) | + | std::plus<float>{}(1.2, 3.4); // est équivalent à (1.2 + 3.4) |
</code> | </code> | ||
- | Dans ce code, ''std::less'' et ''std::plus'' sont des classes (des structures de données) et non des fonctions. Mais il est possible de les appeler comme des fonctions. | + | Dans ce code, ''std::less'' et ''std::plus'' sont des classes template (des structures de données) et non des fonctions. Mais il est possible de les appeler comme des fonctions. |
- | Les algorithmes de la bibliothèque standard sont des bons exemple d'algorithmes dont le comportement peut être modifié en utilisant des fonctions comme arguments. | + | Les algorithmes de la bibliothèque standard sont des bons exemples d'algorithmes dont le comportement peut être modifié en utilisant des fonctions comme arguments. |
- | Prenez par exemple l'algorithme ''std::sort''. Cet algorithme parcourir une collection et compare les elements deux par deux. Par défaut, il utilise le prédicat ''std::less'', qui retourne vrai si le premier argument est plus petit que le second argument. (Pour rappel, un prédicat est un foncteur qui retourne un booléen). Le résultat est une collection dont les éléments sont triés du plus petit au plus grand. | + | Prenez par exemple l'algorithme ''std::sort''. Cet algorithme parcourt une collection et compare les éléments deux par deux. Par défaut, il utilise le prédicat ''std::less'', qui retourne ''true'' si le premier argument est plus petit que le second. (Pour rappel, un prédicat est un foncteur qui retourne un booléen). Le résultat est une collection dont les éléments sont triés du plus petit au plus grand. |
<code cpp> | <code cpp> | ||
Ligne 41: | Ligne 41: | ||
</code> | </code> | ||
- | Pour trier une collection du plus au plus petit, une solution serait d'écrire un nouvel algorithme, qui utilise ''std::greater'' ("plus grand que") comme prédicat. Mais cette solution n'est pas très évolutive : il faut écrire un nouvel algorithme a chaque fois que vous avez besoin d'un nouveau prédicat. | + | Pour trier une collection du grand au plus petit, une solution serait d'écrire un nouvel algorithme qui utilise ''std::greater'' ("plus grand que") comme prédicat. Mais cette solution n'est pas très évolutive : il faut écrire un nouvel algorithme à chaque fois que vous avez besoin d'un nouveau prédicat. |
Une meilleure solution est de faire en sorte que le prédicat soit un argument de l'algorithme. Par exemple, pour trier du plus grand au plus petit, il est possible d'utiliser ''std::greater'' avec ''std::sort''. | Une meilleure solution est de faire en sorte que le prédicat soit un argument de l'algorithme. Par exemple, pour trier du plus grand au plus petit, il est possible d'utiliser ''std::greater'' avec ''std::sort''. | ||
<code cpp> | <code cpp> | ||
- | std::sort(std::begin(v), std::end(v), std::greater); | + | std::sort(std::begin(v), std::end(v), std::greater<type>{}); |
</code> | </code> | ||
Ligne 67: | Ligne 67: | ||
std::vector<Person> persons = make_persons(); | std::vector<Person> persons = make_persons(); | ||
std::sort(std::begin(persons), std::end(persons), less_by_name); // tri selon le nom | std::sort(std::begin(persons), std::end(persons), less_by_name); // tri selon le nom | ||
- | std::sort(std::begin(persons), std::end(persons), less_by_age); // tri selon l'age | + | std::sort(std::begin(persons), std::end(persons), less_by_age); // tri selon l'âge |
</code> | </code> | ||
Ligne 98: | Ligne 98: | ||
</code> | </code> | ||
- | Dans ce code, ''g'' est une variable qui contient la fonction ''f''. Utiliser ''g'' revient donc à utiliser ''f''. ''g'' n'est pas une fonction, c'est une variable. Mais comme elle "contient" une fonction, elle devient un objet appelable et vous pouvez utiliser des parenthèses pour l'appeler. (Une variable ne contient pas a proprement parlé une fonction, mais les détails techniques n'ont pas d'intérêt pour le moment). | + | Dans ce code, ''g'' est une variable qui contient la fonction ''f''. Utiliser ''g'' revient donc à utiliser ''f''. ''g'' n'est pas une fonction, c'est une variable. Mais comme elle "contient" une fonction, elle devient un objet appelable et vous pouvez utiliser des parenthèses pour l'appeler. (Une variable ne contient pas à proprement parlé une fonction, mais les détails techniques n'ont pas d'intérêt pour le moment). |
<code cpp main.cpp> | <code cpp main.cpp> | ||
Ligne 149: | Ligne 149: | ||
Dans le cas ou vous souhaitez utiliser une fonction comme argument d'une autre fonction, vous ne pouvez pas utiliser ''auto'' en paramètre (cela viendra dans une prochaine norme du C++). Il faut donc recourir aux fonctions génériques et aux //templates//. | Dans le cas ou vous souhaitez utiliser une fonction comme argument d'une autre fonction, vous ne pouvez pas utiliser ''auto'' en paramètre (cela viendra dans une prochaine norme du C++). Il faut donc recourir aux fonctions génériques et aux //templates//. | ||
- | La syntaxe d'une fonction générique qui prend une fonction en paramètre est strictement identique aux fonctions générique classiques : un paramètre template peut représenter aussi bien un type de données qu'une fonction. (Pour être plus précis, un paramètre template peut représenter n'importe quel type. Et une fonction est un type comme les autres). | + | La syntaxe d'une fonction générique qui prend une fonction en paramètre est strictement identique aux fonctions génériques classiques : un paramètre template peut représenter aussi bien un type de données qu'une fonction. (Pour être plus précis, un paramètre template peut représenter n'importe quel type. Et une fonction est un type comme les autres). |
<code cpp> | <code cpp> | ||
Ligne 158: | Ligne 158: | ||
</code> | </code> | ||
- | La fonction ''g'' prend en paramètre un type qui devra être appelable (puisque que ''f'' est utilisé comme une fonction dans le corps de ''g'') et qui peut prendre en argument la valeur ''2'' (donc avoir paramètre de type entier ou convertible depuis un entier. Elle peut également avoir d'autres paramètres, mais ils doivent avoir des parametres par defaut). | + | La fonction ''g'' prend en paramètre un type qui devra être appelable (puisque que ''f'' est utilisé comme une fonction dans le corps de ''g'') et qui peut prendre en argument la valeur ''2'' (donc avoir paramètre de type entier ou convertible depuis un entier. Elle peut également avoir d'autres paramètres, mais ils doivent avoir des paramètres par défaut). |
Note : il est classique de nommer un paramètre template ''F'', ''G'', etc. pour les distinguer des autres types ''T'', ''U'', etc. C'est simplement une convention d'écriture, le compilateur ne vérifie pas cela. | Note : il est classique de nommer un paramètre template ''F'', ''G'', etc. pour les distinguer des autres types ''T'', ''U'', etc. C'est simplement une convention d'écriture, le compilateur ne vérifie pas cela. | ||
Ligne 177: | Ligne 177: | ||
</code> | </code> | ||
- | Voici un exemple plus interessant. La fonction ''invoke'' (qui sera ajoutée dans le C++17) prend en paramètre un objet appelable et des valeurs et appelle cet objet en utilisant les valeurs. La fonction ''std::invoke'' du C++17 peut s'utiliser avec une liste quelconque de valeurs, mais cet exemple se limitera à une liste déterminée de valeurs (il faut utiliser des //template variadic// pour faire cela). | + | Voici un exemple plus intéressant. La fonction ''invoke'' (qui sera ajoutée dans le C++17) prend en paramètre un objet appelable et des valeurs et appelle cet objet en utilisant les valeurs. La fonction ''std::invoke'' du C++17 peut s'utiliser avec une liste quelconque de valeurs, mais cet exemple se limitera à une liste déterminée de valeurs (il faut utiliser des //template variadic// pour faire cela). |
La fonction ''invoke'' peut s'écrire de la façon suivante : | La fonction ''invoke'' peut s'écrire de la façon suivante : | ||
Ligne 188: | Ligne 188: | ||
</code> | </code> | ||
- | Note : il est classique aussi de mettre la paramètre template correspondant à un objet appellable en premier. | + | Note : il est classique aussi de mettre le paramètre template correspondant à un objet appelable en premier. |
Pour appeler cette fonction, il faut donner un objet appelable en argument. Cet objet peut être une fonction, un foncteur, une fonction lambda. | Pour appeler cette fonction, il faut donner un objet appelable en argument. Cet objet peut être une fonction, un foncteur, une fonction lambda. | ||
Ligne 198: | Ligne 198: | ||
template<typename T, typename F> | template<typename T, typename F> | ||
- | void invoke(T x, T y, F f) { | + | void invoke(F f, T x, T y) { |
std::cout << f(x, y) << std::endl; | std::cout << f(x, y) << std::endl; | ||
} | } | ||
Ligne 207: | Ligne 207: | ||
int main() { | int main() { | ||
- | invoke(std::plus<int>(), 1, 3); | ||
invoke(minus, 1, 3); | invoke(minus, 1, 3); | ||
+ | invoke(std::plus<int>(), 1, 3); | ||
invoke([](int lhs, int rhs){ return lhs * rhs; }, 1, 3); | invoke([](int lhs, int rhs){ return lhs * rhs; }, 1, 3); | ||
} | } | ||
Ligne 216: | Ligne 216: | ||
===== Type explicite de fonction ===== | ===== Type explicite de fonction ===== | ||
- | Dans certain cas, vous ne pourrez utiliser ni la déduction de type, ni une fonction template (par exemple dans les classes). Il faut dans ce cas écrire explicitement le type de la fonction pour créer une variable. Historiquement, le type d'une fonction vient du langage C et correspond à un pointeur de fonction (une adresse en mémoire de l'ordinateur). La syntaxe est complexe et l'utilisation de pointeurs pose souvent des problemes. | + | Dans certain cas, vous ne pourrez utiliser ni la déduction de type, ni une fonction template (par exemple dans les classes). Il faut dans ce cas écrire explicitement le type de la fonction pour créer une variable. Historiquement, le type d'une fonction vient du langage C et correspond à un pointeur de fonction (une adresse en mémoire de l'ordinateur). La syntaxe est complexe et l'utilisation de pointeurs pose souvent des problèmes. |
- | Le C++11 a heureusement ajouter des fonctionnalités dans la bibliothèque standard pour simplifier la création de fonctions : ''std::function''. Cette classe est définie dans le fichier d'en-tête ''<functional>''. C'est une classe //template//, donc il va falloir utiliser les chevrons encore une fois. | + | Le C++11 a heureusement ajouté des fonctionnalités dans la bibliothèque standard pour simplifier la création de fonctions : ''std::function''. Cette classe est définie dans le fichier d'en-tête ''<functional>''. C'est une classe //template//, donc il va falloir utiliser les chevrons encore une fois. |
La syntaxe générale est la suivante : | La syntaxe générale est la suivante : | ||
Ligne 293: | Ligne 293: | ||
La dernière fonction est ''std::bind''. Cette fonction permet de créer une nouvelle fonction à partir d'une fonction, en modifiant les paramètres (par exemple en donnant une valeur à un paramètre ou en modifiant l'ordre des paramètres). | La dernière fonction est ''std::bind''. Cette fonction permet de créer une nouvelle fonction à partir d'une fonction, en modifiant les paramètres (par exemple en donnant une valeur à un paramètre ou en modifiant l'ordre des paramètres). | ||
- | La fonction ''std::bind'' prend en paramètre une fonction et une liste de paramètre pour appeler cette fonction. La liste des valeurs doit correspondre à la fonction appelée. Par exemple, si une fonction prend quatre paramètres, ''std::bind'' prendra en argument une fonction et quatre paramètres. | + | La fonction ''std::bind'' prend en paramètre une fonction et une liste de paramètres pour appeler cette fonction. La liste des valeurs doit correspondre à la fonction appelée. Par exemple, si une fonction prend quatre paramètres, ''std::bind'' prendra en argument une fonction et quatre paramètres. |
<code cpp> | <code cpp> | ||
Ligne 301: | Ligne 301: | ||
</code> | </code> | ||
- | La fonction ''g'' définie ci-dessus ne représente pas le résultat de l'appel de la fonction ''f'' (comme ca serait le cas avec ''std::invoke'') mais une nouvelle fonction qui appelle ''f'' avec ces valeurs. Cela revient a définir la fonction ''g'' de la facon suivante : | + | La fonction ''g'' définie ci-dessus ne représente pas le résultat de l'appel de la fonction ''f'' (comme ce serait le cas avec ''std::invoke'') mais une nouvelle fonction qui appelle ''f'' avec ces valeurs. Cela revient à définir la fonction ''g'' de la façon suivante : |
<code cpp> | <code cpp> | ||
Ligne 315: | Ligne 315: | ||
</code> | </code> | ||
- | Il est possible de créer une fonction qui prend des paramètres de fonction. Pour cela, il faut utiliser des //placeholders//, qui représente les paramètres de la nouvelle fonction. Ces //placeholders// s'ecrivent ''_1'', ''_2'', etc. et correspondent a premier paramètre, au second paramètre, etc. Les //placeholders// sont définies dans l'espace de noms ''std::placeholders''. | + | Il est possible de créer une fonction qui prend des paramètres de fonction. Pour cela, il faut utiliser des //placeholders// qui représentent les paramètres de la nouvelle fonction. Ces //placeholders// s'écrivent ''_1'', ''_2'', etc. et correspondent au premier paramètre, au second paramètre, etc. Les //placeholders// sont définis dans l'espace de noms ''std::placeholders''. |
Par exemple, pour écrire une fonction ''g'' qui appelle directement la fonction ''f'' : | Par exemple, pour écrire une fonction ''g'' qui appelle directement la fonction ''f'' : | ||
Ligne 325: | Ligne 325: | ||
auto g = std::bind(f, _1, _2, _3, _4); | auto g = std::bind(f, _1, _2, _3, _4); | ||
- | // est equivalent a : | + | // est équivalent à : |
void g(int i1, int i2, int i3, int i4) { | void g(int i1, int i2, int i3, int i4) { | ||
Ligne 340: | Ligne 340: | ||
auto g = std::bind(f, _4, _3, _2, _1); | auto g = std::bind(f, _4, _3, _2, _1); | ||
- | // est equivalent a : | + | // est équivalent à : |
void g(int i1, int i2, int i3, int i4) { | void g(int i1, int i2, int i3, int i4) { | ||
Ligne 355: | Ligne 355: | ||
auto g = std::bind(f, _2, 12, _1, -21); | auto g = std::bind(f, _2, 12, _1, -21); | ||
- | // est equivalent a : | + | // est équivalent à : |
void g(int i1, int i2) { | void g(int i1, int i2) { | ||
Ligne 364: | Ligne 364: | ||
Il faut bien faire attention à l'ordre des paramètres dans ''std::bind'' (qui correspond à l'ordre des paramètres dans la fonction appelée) et l'ordre des //placeholders// (qui correspond à l'ordre des paramètres dans la fonction créée). | Il faut bien faire attention à l'ordre des paramètres dans ''std::bind'' (qui correspond à l'ordre des paramètres dans la fonction appelée) et l'ordre des //placeholders// (qui correspond à l'ordre des paramètres dans la fonction créée). | ||
- | Il est également possible de créer des référence sur des variables en utilisant ''std::ref'' (reference non constant) et ''std::cref'' (reference constante). | + | Il est également possible de créer des référence sur des variables en utilisant ''std::ref'' (référence non constante) et ''std::cref'' (référence constante). |
<code cpp> | <code cpp> | ||
+ | void f(int); | ||
+ | |||
int i { 123 }; | int i { 123 }; | ||
auto g = std::bind(f, std::ref(i)); | auto g = std::bind(f, std::ref(i)); | ||
- | // est equivalent a : | + | // est équivalent à : |
void g(int & i) { | void g(int & i) { | ||
Ligne 377: | Ligne 379: | ||
</code> | </code> | ||
- | En pratique, ''std::bind'' est depreciee par rapport aux fonctions lambdas. Il sera généralement plus simple et lisible d'utiliser les fonctions lambdas. | + | En pratique, ''std::bind'' est dépréciée par rapport aux fonctions lambdas. Il sera généralement plus simple et lisible d'utiliser les fonctions lambdas. |
<code cpp> | <code cpp> | ||
Ligne 385: | Ligne 387: | ||
auto g = std::bind(f, _2, 12, _1, -21); | auto g = std::bind(f, _2, 12, _1, -21); | ||
- | // est equivalent a : | + | // est équivalent à : |
auto g = [](int i1, int i2) { f(i2, 12, i1, -21); }; | auto g = [](int i1, int i2) { f(i2, 12, i1, -21); }; |