Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
callable [2016/12/07 02:01] gbdivers créée |
callable [2019/03/10 11:09] (Version actuelle) alavida [Les fonctions génériques] |
||
---|---|---|---|
Ligne 3: | Ligne 3: | ||
- | ====== [Aller plus loin] Les fonctions comme donnees ====== | + | ====== [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 donnees 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 à 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. | ||
- | ===== Un exemple dans la bibliotheque standard ===== | + | Vous pourrez aussi voir le terme //callback// (fonction de rappel) pour désigner cette approche. |
+ | |||
+ | |||
+ | ===== Un exemple dans la bibliothèque standard ===== | ||
Comme vous l'avez vu, la syntaxe générale pour appeler une fonction est la suivante : | Comme vous l'avez vu, la syntaxe générale pour appeler une fonction est la suivante : | ||
Ligne 18: | 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 donnees) 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 37: | Ligne 41: | ||
</code> | </code> | ||
- | Pour trier une collection du plus au plus petit, une solution serait d'ecrire un nouvel algorithme, qui utilise ''std::greater'' ("plus grand que") comme prédicat. Mais cette solution n'est pas très évolutive : il faut ecrire 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> | ||
- | Il est possible d'aller plus loin et de créer de nouveaux prédicats, qui seront utilisables directement dans les algorithmes standards. Par exemple, si vous créez une structure de donnees : | + | Il est possible d'aller plus loin et de créer de nouveaux prédicats, qui seront utilisables directement dans les algorithmes standards. Par exemple, si vous créez une structure de données : |
<code cpp> | <code cpp> | ||
Ligne 63: | 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> | ||
Cette approche permet d'écrire du code fortement réutilisable : | Cette approche permet d'écrire du code fortement réutilisable : | ||
- | * vous écrivez des structures de donnees indépendamment des algorithmes ; | + | * vous écrivez des structures de données indépendamment des algorithmes ; |
- | * vous écrivez des algorithmes indépendamment des structures de donnees ; | + | * vous écrivez des algorithmes indépendamment des structures de données ; |
- | * vous écrivez des prédicats qui font le lien entre algorithmes et structures de donnees. | + | * vous écrivez des prédicats qui font le lien entre algorithmes et structures de données. |
Ligne 83: | Ligne 87: | ||
La différence avec une variable classique est ce que vous allez utiliser pour ''TYPE'' et ''VALEUR''. | La différence avec une variable classique est ce que vous allez utiliser pour ''TYPE'' et ''VALEUR''. | ||
- | Avec auto | + | |
+ | ==== Déduction de types ==== | ||
+ | |||
+ | La méthode la plus simple, comme souvent, est de laisser le compilateur faire le travail. Il est tout à fait possible de créer une variable contenant une fonction, en utilisant la déduction de type ''auto''. Dans ce cas, la valeur sera l'identifiant de la fonction (son nom, donc sans les parenthèses, les paramètres de fonction ou le type de retour). | ||
+ | |||
+ | <code> | ||
+ | void f() {} | ||
+ | |||
+ | auto g = f; | ||
+ | </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 à 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> | ||
#include <iostream> | #include <iostream> | ||
- | int add(int lhs, int rhs) { | + | void f() { |
- | return lhs + rhs; | + | std::cout << "f()" << std::endl; |
} | } | ||
int main() { | int main() { | ||
- | auto f { add }; | + | auto g = f; |
- | auto x = f(12, 34); | + | g(); |
- | std::cout << x << std::endl; | + | |
} | } | ||
</code> | </code> | ||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | f() | ||
+ | </code> | ||
+ | |||
+ | Dans le cas d'une fonction qui possède des paramètres et un retour, l'appel de la variable est identique à un appel direct de la fonction. | ||
+ | |||
+ | <code cpp main.cpp> | ||
+ | int f(int i) { | ||
+ | std::cout << "f:" << i << std::endl; | ||
+ | return 123; | ||
+ | } | ||
+ | |||
+ | auto g = f; | ||
+ | std::cout << "g:" << g(-1) << std::endl; | ||
+ | </code> | ||
+ | |||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | f:-1 | ||
+ | g:123 | ||
+ | </code> | ||
+ | |||
+ | Cette approche est particulièrement intéressante avec une fonction lambda. | ||
+ | |||
+ | <code cpp> | ||
+ | auto f = [](int i){ std::cout << i << std::endl; }; | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== Les fonctions génériques ==== | ||
+ | |||
+ | 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é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> | ||
+ | template<typename F> | ||
+ | void g(F f) { | ||
+ | f(2); | ||
+ | } | ||
+ | </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 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. | ||
+ | |||
+ | Par exemple : | ||
+ | |||
+ | <code cpp> | ||
+ | void print(int i) { | ||
+ | std::cout << i << std::endl; | ||
+ | } | ||
+ | |||
+ | void signed(int i) { | ||
+ | std::cout << (i > 0 ? "positive" : "negative") << std::endl; | ||
+ | } | ||
+ | |||
+ | g(print); // affiche "2" | ||
+ | g(assert) // affiche "positive" | ||
+ | </code> | ||
+ | |||
+ | 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 : | ||
+ | |||
+ | <code cpp> | ||
+ | template<typename F, typename T> | ||
+ | void invoke(F f, T x, T y) { | ||
+ | std::cout << f(x, y) << std::endl; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | |||
+ | <code cpp main.cpp> | ||
+ | #include <iostream> | ||
+ | #include <functional> | ||
+ | |||
+ | template<typename T, typename F> | ||
+ | void invoke(F f, T x, T y) { | ||
+ | std::cout << f(x, y) << std::endl; | ||
+ | } | ||
+ | |||
+ | int minus(int lhs, int rhs) { | ||
+ | return lhs - rhs; | ||
+ | } | ||
+ | |||
+ | int main() { | ||
+ | invoke(minus, 1, 3); | ||
+ | invoke(std::plus<int>(), 1, 3); | ||
+ | invoke([](int lhs, int rhs){ return lhs * rhs; }, 1, 3); | ||
+ | } | ||
+ | </code> | ||
===== Type explicite de fonction ===== | ===== Type explicite de fonction ===== | ||
- | exemple : bool f(int, double) | + | 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. |
- | En C++, std::function. Template, utiliser signature de fonction. std::function<bool (int, double)> | + | 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. |
- | Note : pointeur de fonction : un peu complexe, heritage du C. bool (*)(int, double); | + | La syntaxe générale est la suivante : |
+ | <code cpp> | ||
+ | std::function< TYPE_RETOUR ( LISTE_TYPES ) > | ||
+ | </code> | ||
- | ===== autres ===== | + | Il faut commencer par donner le type de retour ''TYPE_RETOUR'' ou ''void'' si la fonction ne retourne rien, puis la liste des types des paramètres ''LISTE_TYPES'' séparées par des virgules (ou rien si la fonction ne prend aucun paramètre). |
- | std::bind = cree nouvelle fonction, avec arguments | + | Par exemple, pour une fonction qui prend en paramètre un entier, un double et retourne une chaine : |
- | std::invoke = appel une fonction avec argument | + | <code cpp> |
+ | std::function< std::string ( int, double) > | ||
+ | ^ retour | ||
+ | ^ 1er paramètre | ||
+ | ^ 2nd paramètre | ||
+ | </code> | ||
- | callback | + | Voici quelques exemples : |
+ | ^ ''std::function'' ^ Exemple de fonction ^ | ||
+ | | ''std::function<void()>'' | ''void f();'' | | ||
+ | | ''std::function<int()>'' | ''int f();'' | | ||
+ | | ''std::function<void(int)>'' | ''void f(int i);'' | | ||
+ | | ''std::function<void(int,int)>'' | ''void f(int i, int j);'' | | ||
+ | | ''std::function<int(int,int)>'' | ''int f(int i, int j);'' | | ||
+ | |||
+ | Vous voyez dans ces exemples que dès que vous connaissez la signature de la fonction, il est relativement simple d'écrire le ''std::function'' correspondant. | ||
+ | |||
+ | <code cpp main.cpp> | ||
+ | #include <iostream> | ||
+ | #include <functional> | ||
+ | |||
+ | int add(int i, int j) { | ||
+ | return i + j; | ||
+ | } | ||
+ | |||
+ | int main() { | ||
+ | std::function<int(int,int)> f = add; | ||
+ | const auto i = f(1, 2); // 1 + 2 | ||
+ | std::cout << i << std::endl; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | 3 | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ===== Autres fonctionnalités sur les fonctions ===== | ||
+ | |||
+ | Pour terminer ce chapitre, voici rapidement quelques fonctions utilitaires de la bibliothèque standard pour manipuler les fonctions. | ||
+ | |||
+ | Vous avez vu précédemment la fonction ''std::invoke'' (C++17), pour appeler une fonction avec des arguments. | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int, std::string); | ||
+ | std::invoke(f, 123, "hello"); | ||
+ | </code> | ||
+ | |||
+ | La fonction ''std::apply'' (C++17) est assez proche, sauf qu'elle prend un ''std::tuple'' et applique ses valeurs sur une fonction. | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int, std::string); | ||
+ | std::apply(f, std::make_tuple(123, "hello")); | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== std::bind ==== | ||
+ | |||
+ | 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è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> | ||
+ | void f(int, int, int, int); | ||
+ | |||
+ | auto g = std::bind(f, 1, 2, 3, 4); | ||
+ | </code> | ||
+ | |||
+ | 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> | ||
+ | void g() { | ||
+ | f(1, 2, 3, 4); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Pour appeler la fonction ''g'', il faut donc l'appeler sans paramètre : | ||
+ | |||
+ | <code cpp> | ||
+ | g(); | ||
+ | </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é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'' : | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int, int, int, int); | ||
+ | |||
+ | using namespace std::placeholders; | ||
+ | auto g = std::bind(f, _1, _2, _3, _4); | ||
+ | |||
+ | // est équivalent à : | ||
+ | |||
+ | void g(int i1, int i2, int i3, int i4) { | ||
+ | f(i1, i2, i3, i4); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Il est possible de changer l'ordre des paramètres : | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int, int, int, int); | ||
+ | |||
+ | using namespace std::placeholders; | ||
+ | auto g = std::bind(f, _4, _3, _2, _1); | ||
+ | |||
+ | // est équivalent à : | ||
+ | |||
+ | void g(int i1, int i2, int i3, int i4) { | ||
+ | f(i4, i3, i2, i1); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Ou de mélanger des paramètres et des valeurs : | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int, int, int, int); | ||
+ | |||
+ | using namespace std::placeholders; | ||
+ | auto g = std::bind(f, _2, 12, _1, -21); | ||
+ | |||
+ | // est équivalent à : | ||
+ | |||
+ | void g(int i1, int i2) { | ||
+ | f(i2, 12, i1, -21); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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'' (référence non constante) et ''std::cref'' (référence constante). | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int); | ||
+ | |||
+ | int i { 123 }; | ||
+ | auto g = std::bind(f, std::ref(i)); | ||
+ | |||
+ | // est équivalent à : | ||
+ | |||
+ | void g(int & i) { | ||
+ | f(i); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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> | ||
+ | void f(int, int, int, int); | ||
+ | |||
+ | using namespace std::placeholders; | ||
+ | auto g = std::bind(f, _2, 12, _1, -21); | ||
+ | |||
+ | // est équivalent à : | ||
+ | |||
+ | auto g = [](int i1, int i2) { f(i2, 12, i1, -21); }; | ||
+ | </code> | ||
^ Chapitre précédent ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ Chapitre suivant ^ | ^ Chapitre précédent ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ Chapitre suivant ^ | ||