© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 15/03/2000 -
Dernière mise à jour : 30/03/2001
Remarques préliminaires :
Les deux premières méthodes sont dites faire appel à des itérateurs internes et seront illustrées par différents exercices au cours de ce TP, la troisième utilise des itérateurs externes et sera étudiée au cours du prochain TP.
Recopier les fichiers CLink2Gen.h, CLink2Gen.hxx, CContAbstr.h, CContAbstr.hxx, CStackAbstr.h, CFifoAbstr.h, CDequeAbstr.h, CConteneur_02.h, CConteneur_02.hxx, Makefile_05, soit de votre répertoire dirlstdouble, soit à partir des corrigés.
Recopier les fichiers CCptIdent.h, CCptIdent.hxx, CCptIdent.cxx, soit de votre répertoire dirutilit, soit à partir des corrigés.
Renommer CConteneur_02.h et CConteneur_02.hxx en CConteneur_01.h et CConteneur_01.hxx.
Classe CListAbstr
Cette classe est analogue aux interfaces précédemment écrites, avec des méthodes particulières.
Elle est la classe d'interface d'un conteneur "liste double" permettant de stocker des éléments de type générique T.
Recopier CStackAbstr.h dans CListAbstr_01.h (il y aura plusieurs versions de cette classe).
Remplacer l'identificateur Stack par List.
Conserver la classe d'exceptions CExcList associée à la classe CListAbstr.
Supprimer toutes les méthodes, et ajouter la méthode Look(), de profil :
virtual T * Look (const T & Elem, bool WithInsert = true) throw (CExcList) = 0; |
Les spécifications ci-dessus ajoutées à celles rencontrées précédemment imposent à la classe T d'être dérivable et de posséder :
On peut aussi penser que c'est un défaut d'analyse de faire effectuer des traitements (une recherche par exemple) par un conteneur qui devrait se contenter de contenir ! Une alternative sera envisagée ultérieurement.
Classe CCptIdent
Pour rendre possible l'instanciation de la classe CListe par la classe CCptIdent, celle-ci doit posséder les propriétés indiquées ci-dessus. Il est donc nécessaire de transformer le constructeur en constructeur par défaut.
Classe CConteneur
Implémenter la fonction Look(). Ne disposant d'aucun moyen de vérifier que la liste est correctement remplie, ajouter provisoirement la fonction publique EditerListe(), qui parcourt toute la liste et affiche dans le flux qui lui est passé en paramètre tous les éléments qu'elle contient (sauf bien sûr les sentinelles), en utilisant l'opérateur << supposé surchargé pour la classe T. Cette solution est affreuse et ne sera pas conservée dans les versions ultérieures.
Supprimer du fichier Makefile_01 tout ce qui est inutile, et ajouter les dépendances liées aux classes CCptIdent et CListAbstr.
Dans le fichier exo_01.cxx, instancier la classe CConteneur avec la classe CCptIdent et, en utilisant la fonction Look() avec insertion, insérer des chaînes dans une liste en comptant leur nombre d'apparitions, puis afficher la liste. Tester enfin la fonction Look() sans insertion, sur des chaînes présentes et absentes de la liste.
Compiler et tester.
Corrigés : exo_01.cxx - CListAbstr_01.h - CConteneur_01.h - CConteneur_01.hxx - CCptIdent.h - CCptIdent.hxx - Makefile_01
Il est fréquent que l'utilisateur doive appliquer le même traitement à chaque élément d'une liste. Le conteneur doit donc proposer une méthode qui permet de parcourir tous les éléments et d'effectuer sur chacun le traitement qui lui est passé en paramètre.
Recopier le fichier exo_01.cxx dans exo_02.cxx.
Au début de l'espace anonyme, ajouter la fonction Editer() de profil :
void Editer (CCptIdent &); |
qui affiche dans le flux standard de sortie l'objet CCptIdent qui lui est passé en paramètre.
Recopier le fichier CListAbstr_01.h dans CListAbstr_02.h. Dans la partie publique de la classe CListAbstr, ajouter un type Fct1_t, qui est un pointeur vers une fonction à un paramètre, qui peut s'appliquer à un objet de la classe générique T et ne renvoie rien. Attention au type du paramètre : ignorant la nature du traitement appliqué, nous ne pouvons garantir que le paramètre est const). Ce type est celui de la fonction Editer() écrite ci-dessus. A son interface, ajouter la fonction virtuelle pure ForEach() ayant pour paramètre un pointeur vers la fonction à appliquer (de type Fct1_t).
Recopier les fichiers CConteneur_01.h et CConteneur_01.hxx respectivement dans CConteneur_02.h et CConteneur_02.hxx. Implémenter la fonction ForEach(). Comme c'est elle qui sera utilisée dorénavant pour éditer la liste, la fonction Editer() peut être supprimée.
Recopier le fichier Makefile_01 dans Makefile_02. Ajouter les dépendances liées aux fichiers CStackAbstr_02.h et CConteneur_02.*.
Dans le fichier exo_02.cxx, remplacer les appels à la méthode Editer() de la classe CConteneur par des appels à la fonction ForEach().
Compiler et tester.
Corrigés : exo_02.cxx - CListAbstr_02.h - CConteneur_02.h - CConteneur_02.hxx - Makefile_02
La fonction ForEach() que nous avons écrite est très restrictive, car elle admet comme seul paramètre une fonction qui n'a elle-même comme paramètre qu'un objet de la classe T.
On peut supposer que l'utilisateur soit conduit à effectuer sur chaque élément des traitements à deux paramètres ou plus.
Il est évidemment impossible de définir autant de types de fonctions qu'il y a de nombres et de types de paramètres possibles.
Le C++ offre la possibilité de définir des fonctions dont le profil comporte un nombre variable de paramètres, grâce au symbole ..., appelé "ellipse", comme le montre l'exemple ci-dessous :
template <typename T>
void f (T & elem, ...); |
qui précise que le premier paramètre doit être un objet de type T passé par référence, et qu'il peut y avoir d'autres paramètres de types indéfinis. Cette solution n'est pas applicable dans notre cas, car la fonction ForEach() ne pourrait appeler la fonction f() sans savoir quel(s) paramètre(s) lui passer.
Une technique classique, appelée parfois dans la littérature un crochet [1] (hook), consiste à utiliser un seul paramètre formel supplémentaire, sous la forme d'un pointeur sans type void *. Cela permet de passer comme paramètre effectif un pointeur sur n'importe quel type, donnée ou résultat. En particulier il est possible de passer un pointeur sur une structure (class ou struct) comportant autant de données membres que le souhaite l'utilisateur et de n'importe quels types. Ainsi, en un seul paramètre, l'utilisateur peut faire passer autant d'informations qu'il le souhaite pour le traitement de chaque objet.
Recopier le fichier CListAbstr_02.h dans CListAbstr_03.h. Y ajouter la déclaration du type Fct2_t pour que la fonction de traitement admette un second paramètre comme indiqué ci-dessus. A la classe CListAbstr, ajouter une surcharge de la fonction ForEach(), avec un second paramètre de type void *.
Recopier les fichiers CConteneur_02.h et CConteneur_02.hxx dans les fichiers CConteneur_03.h et CConteneur_03.hxx. Ajouter l'implémentation de la nouvelle fonction ForEach() à deux paramètres.
Recopier le fichier exo_02.cxx dans exo_03.cxx.
La technique du crochet est tout d'abord illustrée en copiant la fonction Editer() écrite précédemment dans la fonction EditerDansFlux(), et en lui ajoutant un paramètre supplémentaire qui est un pointeur vers le flux de sortie à utiliser :
void EditerDansFlux (CCptIdent & Elem, ostream * os); |
Par défaut, tout pointeur est automatiquement transformé par le compilateur en void * si nécessaire.
Le nouveau profil de la fonction EditerDansFlux() devrait donc être compatible avec le type Fct2_t.
L'utilisation de la fonction ForEach() nécessite cependant de transtyper le paramètre effectif de ForEach() - le pointeur vers la fonction - dans le type Fct2_t de la classe CConteneur <CCptIdent>, ce qui peut être effectué au moyen d'une macro paramétrée :
#define CAST(objet) (reinterpret_cast <CConteneur <CCptIdent>::Fct2_t> (objet)) |
dont l'utilisation est la suivante :
UneListe->ForEach (CAST (EditerDansFlux), &cerr); |
Effectuer les modifications de la fonction Exo_03() pour continuer à pouvoir afficher le contenu de la liste avec la fonction Editer() écrite dans l'exercice précédent, et l'afficher aussi dans le flux cerr au moyen de EditerDansFlux().
Après avoir recopié le fichier Makefile_02 dans Makefile_03 et l'avoir modifié pour ajouter les dépendances dues à CConteneur_03.h et CConteneur_03.hxx, compiler et tester.
On désire cumuler les fréquences de tous les identificateurs stockés dans la liste et réaliser l'affichage suivant :
(numéro; identificateur; fréquence; fréquence_cumulée) |
soit, par exemple :
(1, Ch3, 2, 2);
(2, Ch2, 3, 5); (3, Ch1, 3, 8); Nombre total d'identificateurs : 8 |
Dans l'espace anonyme du fichier exo_03.cxx, il faut ajouter :
Dans la fonction Exo_03(), initialiser un objet SCumul, puis appeler la fonction ForEach() en initialisant correctement ses paramètres pour éditer les cumuls dans le format indiqué ci-dessus au moyen de la fonction Cumuler().
Compiler et tester.
Corrigés : exo_03.cxx - CListAbstr_03.h - CConteneur_03.h - CConteneur_03.hxx - Makefile_03
La technique du crochet utilisée ci-dessus est plus proche du C que du C++. En effet, elle utilise un pointeur void * comme paramètre formel de la fonction, ce qui prive le compilateur de tout contrôle possible. De plus, le transtypage systématique mais malheureusement nécessaire rend l'écriture pesante. Le C++ permet une solution d'une grande élégance, qui peut sembler complexe d'un premier abord, mais qui est intensivement utilisée dans la bibliothèque de C++ : les functors. Rappelons qu'un functor est un objet pour lequel l'opérateur fonction () a été surchargé ou, en d'autres termes, un objet qui peut :
Dans les fichiers CFunctor_01.h et CFunctor_01.hxx, écrire la classe CFunctor ayant les caractéristiques suivantes :
ostream * m_os; |
mais cette déclaration obligerait à écrire :
(* m_os) << Elem; |
ce qui n'est pas élégant.
On préfèrera donc que la donnée membre soit une référence à un flux, déclarée ainsi :
ostream & m_os; |
qui permet l'écriture habituelle :
m_os << Elem; |
void operator () (CCptIdent & Element); |
Dans la fonction Exo_04() du fichier exo_04.cxx :
Copier Makefile_03 dans Makefile_04, et ajouter les dépendances dûes aux fichiers CFunctor_01.h et CFunctor_01.hxx.
Compiler et tester.
Corrigés : exo_04.cxx - CFunctor_01.h - CFunctor_01.hxx - Makefile_04
Nous avons montré dans l'exercice précédent qu'un functor peut s'utiliser en lieu et place d'une fonction. Le passage d'un functor en paramètre effectif est plus facile que celui d'un pointeur de fonction, qui nécessite souvent un transtypage. De plus, le corps de l'opérateur () du functor peut accéder non seulement à ses paramètres formels, mais aussi aux données membres de l'objet functor. Enfin, contrairement au passage d'une fonction en paramètre qui ne permet pas de récupérer des résultats, le passage d'un functor en paramètre donnée-résultat permet, en sortie, de récupérer les valeurs de ses données membres, qui peuvent être considérées comme des résultats de l'appel de la fonction.
Dans le prochain exercice, le paramètre de la fonction ForEach() sera remplacé par un functor. Cependant, la classe CFunctor écrite précédemment est trop spécifique au type CCptIdent, et n'est donc pas directement utilisable dans un contexte générique tel que celui des conteneurs. Il faut donc commencer par la généraliser en créant un functor générique. De plus, ignorant totalement ce que devra effectuer l'opérateur (), cette fonction doit être virtuelle pure, la classe de functor générique est donc abstraite.
Dans le fichier CFunctorGen.h, écrire la classe CFunctor1Param de l'espace de noms nsSdD, de paramètre de généricité Param1_t. Déclarer la méthode virtuelle pure operator() dont le paramètre donnée-résultat de type Param_t est passé par référence. Ne pas oublier le destructeur virtuel.
Recopier les fichiers CFunctor_01.h et CFunctor_01_hxx dans CFunctor.h et CFunctor.hxx. Faire dériver la classe CFunctor de CFunctor1Param, et faire les modifications nécessaires.
Recopier le fichier exo_04.cxx dans exo_05.cxx. En principe, la seule modification à effectuer est de remplacer l'inclusion du fichier CFunctor_01.h par CFunctor.h.
Copier Makefile_04 dans Makefile_05, et ajouter les dépendances dûes aux fichiers CFunctorGen.h, CFunctor.h et CFunctor.hxx.
Compiler et tester.
Corrigés : exo_05.cxx - CFunctorGen.h - CFunctor.h - CFunctor.hxx - Makefile_05
Recopier les fichiers CListAbstr_03.h, CConteneur_03.h et CConteneur_03.hxx respectivement dans les fichiers CListAbstr_04.h, CConteneur_04.h et CConteneur_04.hxx. Il est maintenant possible d'ajouter à la classe CListAbstr une surcharge de la fonction ForEach() acceptant pour paramètre un functor générique de même pararmètre de généricité que la classe CListAbstr. Faire les modifications correspondantes dans les trois fichiers.
Recopier le fichier exo_02.cxx dans exo_06.cxx. Supprimer la fonction Editer(), remplacer le fichier CConteneur_02.h par CConteneur_04.h, et inclure le fichier CFunctor.h. Déclarer un objet de la classe CFunctor, et le passer à la fonction ForEach() pour éditer la liste.
Compiler et tester.
Corrigés : exo_06.cxx - CListAbstr_04.h - CConteneur_04.h - CConteneur_04.hxx - Makefile_06
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique