Aller au menu - Aller au contenu

Icône Itérateurs et foncteurs

Mise à jour : 27/05/2011
Difficulté : Difficile Difficile Durée d'étude : 1 jour Creative Commons BY-NC-SA
75 639 visites depuis 7 jours, dont 448 sur ce chapitre classé 5/786
Dans le chapitre précédent, vous avez pu vous familiariser un peu avec les différents conteneurs de la STL.

Vous avez appris à ajouter des éléments à l'intérieur, mais vous n'avez guère fait plus excitant. Vous avez dû rester un peu sur votre faim. Il faut bien sûr apprendre à parcourir les conteneurs et à appliquer des traitements aux éléments. Pour ce faire : nous allons avoir besoin de deux notions, les itérateurs et les foncteurs. :euh:

Les itérateurs sont des objets ressemblant aux pointeurs qui vont nous permettre de parcourir les conteneurs. L'intérêt de ces objets est qu'on les utilise de la même manière quel que soit le conteneur ! Pas besoin de faire de distinction entre les vector, les map ou les list. Vous allez voir, c'est magique.

Les foncteurs, quant à eux, sont des objets que l'on va utiliser comme fonction. Nous allons alors pouvoir appliquer ces fonctions à tous les éléments d'un conteneur par exemple.

Prenez une grande inspiration, ce chapitre va changer beaucoup de choses dans votre vie ! ^^
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Itérateurs : des pointeurs boostés

Dans les premiers chapitres de ce cours, nous avions vu que les pointeurs pouvaient être assimilés à des flèches pointant sur les cases de la mémoire de l'ordinateur. Ce n'est bien sûr qu'une image, mais elle va nous aider dans la suite. ;)
Un conteneur est un objet contenant des éléments. Un peu comme la mémoire contient des variables. Les concepteurs de la STL ont donc eu l'idée de créer des pointeurs spéciaux pour se déplacer dans les conteneurs comme le ferait un pointeur dans la mémoire. Ces pointeurs spéciaux s'appellent des itérateurs.

Les itérateurs sont en réalité des objets plutôt complexes. Pas juste de simples pointeurs.

L'avantage de cette manière de faire est qu'elle réutilise quelque chose que l'on connaît bien. On peut déplacer l'itérateur en utilisant les opérateurs ++ et --, comme on pourrait le faire pour un pointeur. Mais l'analogie ne s'arrête pas là, on accède à l'élément pointé (ou itéré ;) ) via l'étoile *. Bref, ça nous rappelle de vieux souvenirs. Du moins j'espère...

Déclarer un itérateur...



Chaque conteneur possède son propre type d'itérateur, mais la manière de les déclarer est toujours la même. Comme toujours, il faut un type et un nom. Choisir un nom, c'est votre problème ;) , mais pour le type, je vais vous aider. Il faut mettre le type du conteneur, suivi de l'opérateur :: et du mot iterator. Par exemple pour un itérateur sur un vector d'entiers, on a :

Code : C++ - Itérateur sur un vector d'entiers
1
2
3
4
5
#include <vector>
using namespace std;

vector<int> tableau(5,4);     //Un tableau de 5 entiers valant 4
vector<int>::iterator it;     //Un itérateur sur un vector d'entiers


Voici encore quelques exemples:

Code : C++ - D'autres itérateurs
1
2
3
4
5
map<string, int>::iterator it1; //Un itérateur sur les tables associatives string-int

deque<char>::iterator it2;      //Un itérateur sur une deque de caractères 

list<double>::iterator it3;     //Un itérateur sur une liste de nombres à virgule


Bon. Je crois que vous avez compris. ^^

... et itérer



Il ne nous reste plus qu'à les utiliser. Tous les conteneurs possèdent une méthode begin() renvoyant un itérateur sur le premier élément contenu. On peut ainsi faire pointer l'itérateur sur le premier élément. On avance alors dans le conteneur en utilisant l'opérateur ++. Il ne nous reste plus qu'à spécifier une condition d'arrêt. On ne veut pas aller en-dehors du conteneur. Pour ce faire, les conteneurs possèdent une méthode end() renvoyant un itérateur sur la fin du conteneur.

En réalité, end() renvoie un itérateur sur un élément en-dehors du conteneur. Il faut donc itérer jusqu'à end() non compris.


On peut donc parcourir un conteneur en itérant dessus depuis begin() jusqu'à end(). Voyons ça avec un exemple :

Code : C++ - Itérer sur une deque
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include<deque>
#include <iostream>
using namespace std;

int main()
{
    deque<int> d(5,6);        //Une deque de 5 éléments valant 6
    deque<int>::iterator it;  //Un itérateur sur une deque d'entiers

    //Et on itère sur la deque
    for(it = d.begin(); it!=d.end(); ++it)
    {
        cout << *it << endl;    //On accède à l'élément pointé via l'étoile
    }
    return 0;
}


Les itérateurs ne sont pas optimisés pour l'opérateur de comparaison. On ne devrait donc pas écrire it<d.end() comme on en a l'habitude avec les index de tableau. Utiliser != est plus efficace.


Simple non ? Si vous avez aimé les pointeurs ( :p ), vous allez adorer les itérateurs. Pour les vector et les deque, cela peut vous sembler inutile. On peut faire aussi bien avec les crochets []. Mais pour les map et surtout les list, ce n'est pas vrai, les itérateurs sont le seul moyen que nous avons de les parcourir.

Des méthodes uniquement pour les itérateurs



Même pour les vector ou deque, il existe des méthodes qui nécessitent l'emploi d'itérateurs. Il s'agit en particulier des méthodes insert() et erase() qui, comme leur nom l'indique, permettent d'ajouter ou supprimer un élément au milieu d'un conteneur. Jusqu'à maintenant, vous ne pouviez qu'ajouter des éléments à la fin d'un conteneur, jamais au milieu. La raison est simple : pour ajouter quelque chose au milieu, il faut indiquer l'on souhaite insérer l'élément. Et ça, c'est justement le but d'un itérateur.

Un exemple vaut mieux qu'un long discours.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <vector>
#include <string>
#include <iostream>
using namespace std;

int main()
{
    vector<string> tab;    //Un tableau de mots

    tab.push_back("les"); //On ajoute deux mots dans le tableau
    tab.push_back("Zeros");

    tab.insert(tab.begin(), "Salut"); //On insère le mot "Salut" au début

    //Affiche les mots donc la chaine "Salut les Zeros"
    for(vector<string>::iterator it=tab.begin(); it!=tab.end(); ++it)
    {
        cout << *it << " ";
    }

    tab.erase(tab.begin()); //On supprime le premier mot

    //Affiche les mots donc la chaine "les Zeros"
    for(vector<string>::iterator it=tab.begin(); it!=tab.end(); ++it)
    {
        cout << *it << " ";
    }

    return 0;
}


Et c'est la même chose pour tous les types de conteneurs. Si vous avez un itérateur sur un élément, vous pouvez le supprimer via erase() ou ajouter un élément juste après grâce à insert().

Souvenez-vous quand même que les vector ne sont pas optimisés pour l'insertion et l'effacement au milieu. Le schéma du chapitre précédent vous aidera à faire un meilleur choix si vous avez vraiment besoin de faire ce genre de modifications sur votre conteneur.


Je vous avais dit que vous alliez adorer ce chapitre ! Et ça ne fait que commencer. ;)

Les différents itérateurs



Terminons quand même avec quelques aspects un petit peu plus techniques. Il existe en réalité 5 sortes d'itérateurs. Lorsque l'on déclare un vector::iterator ou un map::iterator, on déclare en réalité un objet d'une de ces cinq catégories. Cela se fait via une redéfinition de type, chose que nous verrons dans la cinquième partie de ce cours.
Parmi les 5 types d'itérateurs, seuls deux sont utilisés pour les conteneurs : les bidirectional iterators et les random access iterators. Voyons ce qu'ils nous proposent.

Les bidirectional iterators



Ce sont les plus simples des deux. "Bidirectional iterator" signifie itérateur bidirectionnel, mais cela ne nous avance pas beaucoup... :p
Ce sont des itérateurs qui permettent d'avancer et de reculer sur le conteneur. Cela veut dire que vous pouvez utiliser aussi bien ++ que --. L'important étant que l'on ne peut avancer que d'un seul élément à la fois. Donc pour accéder au 6ème d'un conteneur, il faut partir de la position begin() puis appeler cinq fois l'opérateur ++.

Ce sont les itérateurs utilisés pour les list, set et map. On ne peut donc pas utiliser ces itérateurs pour accéder directement au milieu d'un de ces conteneurs.

Les random access iterators



Au vu du nom, vous vous en doutez peut-être, ces itérateurs permettent d'accéder au hasard, ce qui en meilleur français veut dire que l'on peut accéder directement au milieu d'un conteneur.
Techniquement, ces itérateurs proposent en plus de ++ et -- des opérateurs + et - permettant d'avancer d'un coup de plusieurs éléments.

Par exemple pour accéder au 8ème élément d'un vector, on peut utiliser la syntaxe suivante :

Code : C++
1
2
3
vector<int> tab(100,2);  //Un tableau de 100 entiers valant 2

vector<int>::iterator it = tab.begin() + 7; //Un itérateur sur le 8ème élément


En plus des vector, ces itérateurs sont ceux utilisés par les deque.

Le mécanisme exact des itérateurs est très compliqué, c'est pour cela que je ne vous présente que les éléments qui vous seront réellement nécessaires dans la suite. Savoir que certains itérateurs sont plus limités que d'autres nous sera utile au chapitre suivant puisque certains algorithmes ne sont utilisables qu'avec des random access iterators.

La pleine puissance des list et map

Je ne vous ai pas encore parlé des listes chaînées de type list. C'est un conteneur assez différent de ce que vous connaissez. Les éléments ne sont pas rangés les uns à côté des autres dans la mémoire. Chaque "case" contient un élément et un pointeur sur la prochaine case située ailleurs dans la mémoire, comme sur l'illustration suivante :

Image utilisateur


L'avantage de cette structure de données est que l'on peut facilement ajouter des éléments au milieu. Il n'est pas nécessaire de décaler toute la suite comme dans l'exemple de la bibliothèque du chapitre précédent. Mais, (il y a toujours un mais) on ne peut pas directement accéder à une case donnée... tout simplement parce qu'on ne sait pas où elle se trouve dans la mémoire. :euh: On est obligé de suivre toute la chaîne des éléments. Pour aller à la 8ème case, il faut aller à la première case, suivre le pointeur jusqu'à la deuxième, suivre le pointeur jusqu'à la troisième et ainsi de suite jusqu'à la 8ème. C'est donc très couteux.

Passer de case en case dans l'ordre est une mission parfaite pour les itérateurs. :soleil: Et puis, il n'y a pas d'opérateur [] pour les listes. On n'a donc pas le choix !
L'avantage c'est que tout se passe comme pour les autres conteneurs. C'est ça la magie des itérateurs. On n'a pas besoin de connaître les spécificités du conteneur pour itérer dessus.

Code : C++ - Manipuler une liste
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <list>
#include <iostream>
using namespace std;

int main()
{
    list<int> liste;       //Une liste d'entiers
    liste.push_back(5);    //On ajoute un entier dans la liste
    liste.push_back(8);    //Et un deuxième
    liste.push_back(7);    //Et encore un !
    
    //On itère sur la liste
    for(list<int>::iterator it = liste.begin(); it!=liste.end(); ++it)
    {
        cout << *it << endl;
    }
    return 0;
}


Super non ?

La même chose pour les map



La structure interne des map est encore plus compliquée que celle des list. Elles utilisent ce qu'on appelle des arbres binaires et se déplacer dans cet arbre peut vite devenir un vrai casse-tête. Grâce aux itérateurs, ce n'est pas à vous de vous préoccuper de tout ça. Vous utilisez simplement les opérateurs ++ et -- et l'itérateur saute d'élément en élément. Toutes les opérations complexes sont masquées à l'utilisateur.

Il y a juste une petite subtilité avec les tables associatives. Chaque élément est en réalité constitué d'une clé et d'une valeur. Un itérateur ne peut pointer que sur une seule chose à la fois. Il y a donc, a priori, un problème. :( Rien de grave je vous rassure.
Les itérateurs pointent en réalité sur des pair. Ce sont des objets avec deux attributs publics appelés first et second. Les pair sont déclarées dans le fichier d'en-tête utility. Il est cependant très rare de devoir utiliser directement ce fichier puisqu'il est inclus par presque tous les autres. ;) Créons quand même une paire juste pour essayer.

Code : C++ - Utilisation d'une paire
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <utility>
#include <iostream>
using namespace std;

int main()
{
    pair<int, double> p(2, 3.14);    //Une paire contenant un entier valant 2 et un nombre à virgule valant 3.14

    cout << "La paire vaut (" << p.first << ", " << p.second << ")" << endl;

    return 0;
}


Et c'est tout ! o_O On ne peut rien faire d'autre avec une paire. Elles servent juste à contenir deux objets.

Les deux attributs sont publics. Cela peut vous sembler bizarre puisque je vous ai conseillé de toujours déclarer vos attributs dans la partie privée de la classe. Les pair sont là juste pour contenir deux variables d'un coup. Il n'y a aucune méthode ni rien dans la classe. C'est juste un outil très basique et on n'a pas envie de s'embêter avec des méthodes get() et set(). C'est pour cela que les attributs sont publics.


Dans une map, les objets stockés sont en réalité des pair. Pour chaque paire, l'attribut first correspond à la clé alors que second est la valeur.
Je vous ai dit dans le chapitre précédent que les map triaient leurs éléments selon leurs clés. Nous allons maintenant pouvoir le vérifier facilement.

Code : C++ - Itération sur une map
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
    map<string, double> poids; //Une table qui associe le nom d'un animal à son poids
    
    //On ajoute les poids de quelques animaux
    poids["souris"] = 0.05;
    poids["tigre"] = 200;
    poids["chat"] = 3;
    poids["elephant"] = 10000;

    //Et on parcourt la table en affichant le nom et le poids
    for(map<string, double>::iterator it=poids.begin(); it!=poids.end(); ++it)
    {
        cout << it->first << " pese " << it->second << " kg." << endl;
    }
    return 0;
}


Si vous testez, vous verrez que les animaux sont affichés par ordre alphabétique même si on les a inséré dans un tout autre ordre :

Code : Console
chat pese 3 kg.
elephant pese 10000 kg.
souris pese 0.05 kg.
tigre pese 200 kg.


La map utilise l'opérateur < de la classe string pour trier ses éléments. Nous verrons dans la suite comment changer ce comportement.


Les itérateurs sont aussi utiles pour rechercher quelque chose dans une table associative. Utiliser l'opérateur [] permet d'accéder à un élément donné. Mais il a un "défaut". Si l'élément n'existe pas, l'opérateur [] le crée. On ne peut pas l'utiliser pour savoir si un élément donné est déjà présent dans la table ou pas.

C'est pour palier ce problème que les map proposent une méthode find() qui renvoie un itérateur sur l'élément recherché. Si l'élément n'existe pas, elle renvoie simplement end(). Vérifier si une clé existe déjà dans une table est donc très simple.

Reprenons la table de l'exemple précédent et vérifions si le poids d'un chien s'y trouve.

Code : C++ - Recherche dans une map
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
    map<string, double> poids; //Une table qui associe le nom d'un animal à son poids
    
    //On ajoute les poids de quelques animaux
    poids["souris"] = 0.05;
    poids["tigre"] = 200;
    poids["chat"] = 3;
    poids["elephant"] = 10000;

    map<string, double>::iterator trouve = poids.find("chien");

    if(trouve == poids.end())
    {
        cout << "Le poids du chien n'est pas dans la table" << endl;
    }
    else
    {
        cout << "Le chien pese " << trouve->second << " kg." << endl;
    }
    return 0;
}


Je crois ne pas avoir besoin d'en dire plus. ;) Je sens que vous êtes déjà des fans des itérateurs.

Foncteur : la version objet des fonctions

Si vous suivez un cours d'informatique à l'université, on vous dira que les itérateurs sont des abstractions des pointeurs et que les foncteurs sont des abstractions des fonctions. Et généralement, le cours va s'arrêter là. :o
Je pourrais faire de même et vous laisser vous débrouiller avec un ou deux exemples, mais je ne pense pas que vous seriez très heureux.

Ce que l'on aimerait faire, c'est appliquer des changements sur des conteneurs. Par exemple prendre un tableau de lettres et les convertir toutes en majuscule. Ou prendre une liste de nombres et ajouter 5 à tous les nombres pairs. Bref, on aimerait appliquer une fonction sur tous les éléments d'un conteneur. Le problème c'est qu'il faudrait pouvoir passer cette fonction en argument d'une méthode du conteneur. Et ça, on ne sait pas le faire. On ne peut passer que des objets en argument et pas des fonctions.

Techniquement, ce n'est pas vrai. Il existe des pointeurs sur des fonctions et l'on pourrait utiliser ces pointeurs pour résoudre ce problème. Les foncteurs sont par contre plus simples d'utilisation et offrent plus de possibilités.


Les foncteurs sont des objets possédant une surcharge de l'opérateur (). Ils peuvent ainsi agir comme une fonction mais peuvent être passés en argument à une méthode ou à une autre fonction.

Créer un foncteur



Un foncteur est une classe possédant si nécessaires des attributs et des méthodes. Mais en plus de ça, elle doit proposer un opérateur () qui effectue l'opération que l'on souhaite.
Commençons avec un exemple simple, un foncteur qui additionne deux entiers.

Code : C++ - Un premier foncteur
1
2
3
4
5
6
7
8
class Addition{
public:
    
    int operator()(int a, int b)   //La surcharge de l'opérateur ()
    {
        return a+b;
    }
};


Cette classe ne possède pas d'attributs et juste une seule méthode, la fameuse surcharge de l'opérateur (). Comme il n'y a pas d'attributs et rien de spécial à effectuer, le constructeur généré par le compilateur est largement suffisant.

Vous aurez reconnu la syntaxe habituelle pour les opérateurs : le mot operator suivi de l'opérateur que l'on veut, ici les parenthèses. La particularité de cet opérateur est qu'il peut prendre autant d'arguments que l'on veut au contraire de tous les autres qui ont un nombre d'arguments fixé.


On peut alors utiliser ce foncteur pour additionner deux nombres :

Code : C++ - Utilisation du foncteur
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
using namespace std;

int main()
{
    Addition foncteur;
    int a(2), b(3);
    cout << a << " + " << b << " = " << foncteur(a,b) << endl; //On utilise le foncteur comme si il s'agissait d'une fonction
    return 0;
}


Ce code donne bien évidemment le résultat escompté :

Code : Console
2 + 3 = 5


Et l'on peut bien sûr créer tout ce que l'on veut comme foncteur. Par exemple, un foncteur ajoutant 5 aux nombres pairs peut être écrit comme suit :

Code : C++ - Un foncteur qui ajoute 5 aux nombres pairs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Ajout{
public:
    
    int operator()(int a)   //La surcharge de l'opérateur ()
    {
        if(a%2 == 0)
            return a+5;
        else
            return a;
    }
};


Rien de neuf en somme ! :)

Des foncteurs évolutifs



Les foncteurs sont des objets. Ils peuvent donc utiliser des attributs comme n'importe quelle autre classe. Cela nous permet en quelque sorte de créer des fonctions avec une mémoire. Elle pourra donc effectuer une opération différente à chaque appel. Je pense qu'un exemple sera plus parlant.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Remplir{
public:
    Remplir(int i)
        :m_valeur(i)
    {}

    int operator()()
    {
        ++m_valeur;
        return m_valeur;
    }

private:
    int m_valeur;
};


La première chose à remarquer est que notre foncteur possède un constructeur. Son but est simplement d'initialiser correctement l'attribut m_valeur. L'opérateur parenthèse renvoie simplement la valeur de cet attribut, mais ce n'est pas tout. Il incrément cet attribut à chaque appel. Notre foncteur renvoie donc une valeur différente à chaque appel !

On peut par exemple l'utiliser pour remplir un vector avec les nombres de 1 à 100. Je vous laisse essayer. :p

Bon, comme c'est encore une notion récente pour vous, je vous propose quand même une solution :

Code : C++ - Utiliser Remplir pour remplir un tableau
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main()
{ 
    vector<int> tab(100,0); //Un tableau de 100 cases valant toutes 0

    Remplir f(0);       

    for(vector<int>::iterator it=tab.begin(); it!=tab.end(); ++it)
    {
        *it = f(); //On appelle simplement le foncteur sur chacun des éléments du tableau
    }
    
    return 0;
}


Ceci n'est bien sûr qu'un exemple tout simple. On peut créer des foncteurs avec beaucoup d'attributs et des comportement bien plus complexes. On peut aussi ajouter d'autres méthodes pour ré-initialiser m_valeur par exemple. Comme ce sont des objets, tout ce que vous savez à leur sujet reste valable !

Si vous connaissez le C, vous aurez peut-être pensé au mot-clé static qui autorise le même genre de choses pour les fonctions normales. Le foncteur avec des attributs est la manière de réaliser cela en C++.


Les prédicats



Je sens que vous êtes un peu effrayé par ce nouveau nom barbare. C'est vrai que ce chapitre présente plein de nouvelles choses et qu'il faut un peu de temps pour tout assimiler. Rien de bien compliqué ici, je vous rassure. :)

Les prédicats sont des foncteurs un peu particuliers. Ce sont des foncteurs prenant un seul argument et renvoyant un booléen. Ils servent à tester une propriété particulière de l'objet passé en argument. On les utilise pour répondre à des questions comme :
  • Ce nombre est-il plus grand que 10 ?
  • Cette chaîne de caractère contient-elle des voyelles ?
  • Ce Personnage est-il encore vivant ?

Ces prédicats seront très utiles dans la suite. Nous verrons dans le chapitre suivant comment supprimer des objets qui vérifient une certaine propriété. Et c'est bien sûr un foncteur de ce genre qu'il faudra utiliser !
Voyons quand même un petit code avant d'aller plus loin. Prenons le cas d'un prédicat qui teste si une chaîne de caractère contient des voyelles.

Code : C++ - Mon premier prédicat
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestVoyelles
{
public:
    bool operator()(string const& chaine) const
    {
        for(int i(0); i<chaine.size(); ++i)
        {
            switch (chaine[i])   //On teste les lettres une-à-une
            {
                case 'a':        //Si c'est une voyelle
                case 'e':
                case 'i':
                case 'o':
                case 'u':
                case 'y':
                    return true;  //On renvoie 'true'
                default:
                    break;        //Sinon, on continue
            }
        }
        return false;   //Si on arrive là, c'est qu'il n'y avait pas de voyelles du tout
    }
};


Nous verrons dans la suite comment écrire cela de manière plus simple !


Terminons cette sous-partie en jetant un œil à quelques foncteurs pré-définis dans la STL. Eh oui, il y en a même pour les fainéants ! :p

Les foncteurs pré-définis



Pour les opérations les plus simples, le travail est pré-mâché. Tout se trouve dans le fichier d'en-tête functional. Je ne vais pas vous présenter tout ce qui s'y trouve dans ce tuto. Je vous propose de faire un tour dans la documentation, cela vous apprendra à vous débrouiller avec ces sites webs.

Prenons tout de même un exemple. Le premier foncteur que je vous ai présenté prenait deux entiers en argument et renvoyait la somme des nombres. La STL propose un foncteur nommé plus (quelle originalité ;) ) pour faire ça.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>
#include <functional>    //Ne pas oublier !
using namespace std;

int main()
{
    plus<int> foncteur;    //On déclare le foncteur additionnant deux entiers
    int a(2), b(3);
    cout << a << " + " << b << " = " << foncteur(a,b) << endl; //On utilise le foncteur comme si il s'agissait d'une fonction
    return 0;
}


Comme pour les conteneurs, il faut indiquer le type souhaité entre les chevrons. En utilisant ces foncteurs pré-définis, on s'économise un peu de travail. :)

Voyons finalement comment utiliser ces foncteurs avec des conteneurs.

Fusion des deux concepts

Les foncteurs sont au cœur de la STL. Ils sont très utilisés dans les algorithmes que nous verrons dans le prochain chapitre. Pour l'instant, nous allons modifier le critère de tri des map grâce à un foncteur.

Modifier le comportement d'une map



Le constructeur de la classe map prend en réalité un argument : le foncteur de comparaison entre les clés. Par défaut, si l'on ne spécifie rien, c'est un foncteur construit à partir de l'opérateur < qui sert de comparaison. La map que nous avons utilisée précédemment utilisait ce foncteur par défaut.
L'opérateur < pour les string compare les chaînes par ordre alphabétique. Changeons ce comportement pour utiliser une comparaison de longueur. Je vous laisse essayer d'écrire un foncteur comparant la longueur de deux string.

Voici ma solution :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <string>
using namespace std;

class CompareLongueur
{
public:
    bool operator()(const string& a, const string& b)
    {
        return a.length() < b.length();
    }
};


Je pense que vous avez écrit quelque chose de similaire. ;)

Il ne reste maintenant plus qu'à indiquer à notre map que nous voulons utiliser ce foncteur.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int main()
{
  //Une table qui associe le nom d'un animal à son poids
  map<string, double,CompareLongueur> poids;  //On utilise le foncteur comme critère de comparaison
        

  //On ajoute les poids de quelques animaux                                     
  poids["souris"] = 0.05;
  poids["tigre"] = 200;
  poids["chat"] = 3;
  poids["elephant"] = 10000;

  //Et on parcourt la table en affichant le nom et le poids                     
  for(map<string, double>::iterator it=poids.begin(); it!=poids.end(); ++it)
  {
      cout << it->first << " pese " << it->second << " kg." << endl;
  }
  return 0;
}


Et ce programme donne le résultat suivant :

Code : Console
chat pese 3 kg.
tigre pese 200 kg.
souris pese 0.05 kg.
elephant pese 10000 kg.


Les animaux ont été triés selon la longueur de leur nom. :) Changer le comportement d'un conteneur est donc une opération très simple à réaliser.

Récapitulatif des conteneurs les plus courants



Dans le chapitre suivant, nous allons utiliser plusieurs conteneurs différents et comme tout ça est encore un peu nouveau pour vous, voici un petit tableau récapitulatif des 5 conteneurs les plus courants.

ConteneurCaractéristiquesExempleSchéma
vector
  • Éléments stockés côte-à-côte.
  • Optimisé pour l'ajout en fin de tableau.
  • Éléments indexés par des entiers.
vector<int> Image utilisateur
deque
  • Éléments stockés côte-à-côte.
  • Optimisé pour l'ajout en début et en fin de tableau.
  • Éléments indexés par des entiers.
deque<int> Image utilisateur
list
  • Éléments stockés de manière "aléatoire" dans la mémoire.
  • Ne se parcourt qu'avec des itérateurs.
  • Optimisé pour l'insertion et la suppression au milieu.
list<int> Image utilisateur
map
  • Éléments indexés par ce que l'on veut.
  • Éléments triés selon leurs index.
  • Ne se parcourt qu'avec des itérateurs.
map<string,int> Image utilisateur
set
  • Éléments triés.
  • Ne se parcourt qu'avec des itérateurs.
set<int> Image utilisateur

Q.C.M.

Quelle méthode permet d'obtenir un itérateur sur le premier élément d'un conteneur ?
Peut-on accéder directement aux éléments placés au centre d'une list ?
Qu'est-ce qu'un prédicat ?
Quel(s) en-tête(s) faut-il inclure pour utiliser les foncteurs pré-définis de la STL ?

Statistiques de réponses au QCM

Ce chapitre ne présente que la base des itérateurs. Il y aurait encore beaucoup de choses à dire sur le sujet. :soleil: Mais pour l'utilisation que nous allons en faire dans la suite, vous savez tout.

Dans le chapitre suivant, nous allons découvrir une série d'opérations que l'on peut effectuer sur les éléments d'un conteneur. Et bien sûr les itérateurs et foncteurs vont apparaître.
Chapitre précédent Sommaire Chapitre suivant

Partager

4 commentaires pour "Itérateurs et foncteurs"
Note moyenne : 3.85 / 4 (1749 votes)
Pseudo Commentaire
Hors ligne Mordore # Posté le 29/05/2011 à 22:13:05

Vraiment un excellent chapitre !!
Je suis débutant en c++ (j'ai suivis vos cours) et je suis très content de découvrir ces objets tellement pratiques : les itérateurs et les foncteurs. J'avais déjà découvert les itérateurs mais je ne connaissais pas du tout l'existence de l'opérateur (). Je réfléchissais justement à des moyens que je pourrais employer pour mon projet et les foncteurs sont au-dessus de mes espérances ! Utiliser des fonctions à mémoire est vraiment un élément capital je pense : cela permet au programme d'être davantage générique et optimisé.
Merci à vous deux pour avoir présenté ces concepts par "nécessité" : on comprend davantage leur utilité en programmation :)
Hors ligne bddh # Posté le 21/06/2011 à 19:57:12

Chipotons. Dans la sous partie : Les différents itérateurs, on trouve "conteur" au lieu de "conteneur"
Hors ligne Innocenti # Posté le 05/11/2011 à 19:13:30

Dans ce morceau de code :
Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main()
{ 
    vector<int> tab(100,0); //Un tableau de 100 cases valant toutes 0

    Remplir f(0);       

    for(vector<int>::iterator it=tab.begin(); it!=tab.end(); ++it)
    {
        *it = f(); //On appelle simplement le foncteur sur chacun des éléments du tableau
    }
    
    return 0;
}


Je n'ai pas compris la ligne *it = f(); en fait, qu'est-ce que ça fait?
Hors ligne Mechouille # Posté le 06/05/2012 à 00:17:52

Salut,
Voilà j'ai eu un soucis à la compilation du derniers morceau de code proposé,
ci-dessous la correction que j'ai du apporté (j'ai du ajouté dans la déclaration
de l'itérateur dans la boucle for le paramètre CompareLongueur)
Au passage ce tuto est vraiment excellent merci beaucoup pour tout ce travail et ce partage

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int main()
{
  //Une table qui associe le nom d'un animal à son poids
  map<string, double,CompareLongueur> poids;  //On utilise le foncteur comme critère de comparaison
        

  //On ajoute les poids de quelques animaux                                     
  poids["souris"] = 0.05;
  poids["tigre"] = 200;
  poids["chat"] = 3;
  poids["elephant"] = 10000;

  //Et on parcourt la table en affichant le nom et le poids                     
  for(map<string, double, CompareLongueur>::iterator it=poids.begin(); it!=poids.end(); ++it)
  {
      cout << it->first << " pese " << it->second << " kg." << endl;
  }
  return 0;
}

Voir tous les commentaires