C++ : TP 12 - Conteneurs : la liste double (1)

© 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 :

Sommaire

Conteneur "Liste"  exo_01
Fonction ForEach() de base  exo_02
Fonction ForEach() et technique du hook  exo_03
Premier functor  exo_04
functor générique  exo_05
Fonction ForEach() et functor générique  exo_06


Conteneur "Liste"

exo_01

    L'objectif de cet exercice est d'ajouter une interface de liste chaînée à la classe CConteneur déjà écrite, et de l'utiliser en créant une liste de compteurs de chaînes de type CCptIdent.

    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;

Remarques :
  1. l'utilisateur ne doit pas pouvoir modifier les chaînages des éléments de la liste. C'est pourquoi il doit recevoir de la fonction Look() un pointeur T * et non un pointeur CLinkT *.
  2. puisque la liste ci-dessus ne suppose aucun ordre particulier des éléments, l'insertion doit être effectuée le plus facilement possible, c'est-à-dire en tête ou en queue.
  3. une implémentation plus réaliste nécessiterait de pouvoir aussi supprimer des éléments.

    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 :

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

Sommaire


Fonction ForEach() de base

exo_02

    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

Sommaire


Fonction ForEach() et technique du hook

exo_03

    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

Sommaire


Premier functor

exo_04

    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 :

    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

Sommaire


functor générique

exo_05

    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

Sommaire


Fonction ForEach() et functor générique

exo_06

    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

Sommaire


[1] Cette technique est systématiquement utilisée dans le traitement des messages en programmation sous Windows, ainsi que par différentes fonctions de manipulation des threads en C.


Téléchargement des corrigés :

tpliste.zip

Chemins d'accès aux sources des corrigés :

~mathieu/PARTAGE/src/tp/tpC++/tplstdouble/dirlstdouble
~mathieu/PARTAGE/src/tp/tpC++/tputilit/dirutilit
~mathieu/PARTAGE/src/tp/tpC++/tpliste/dirliste

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique