<iterator> : conception d'iterator, introduction aux traits

<iterator> : conception d'iterator, introduction aux traits - C++ - Programmation

Marsh Posté le 31-01-2005 à 21:55:28    

Pré-requis :
- savoir ce qu'est un iterator
- spécialisation partielle
- mot-clef typename

 

Et oui, vous ne le saviez pas, mais <iterator>, c'est un entête standard. Qu'est-ce qu'il y a dedans ?
- des définitions, classes et fonctions relatives aux iterators.
- des "traits" pour iterator.
 
Voici une petite description de ce contenu et j'en profite pour faire une introduction aux traits et quelques rappels sur comment bien faire un iterator.

 
 
Un iterator au sens STL, doit obéir à une certaine "interface". Celle-ci est simple :
 

Code :
  1. template<class Category, class Ty, class Diff = ptrdiff_t
  2.     class Pointer = Ty *, class Reference = Ty&>
  3.     struct iterator {
  4.     typedef Category iterator_category;
  5.     typedef Ty value_type;
  6.     typedef Diff difference_type;
  7.     typedef Pointer pointer;
  8.     typedef Reference reference;
  9.     };


     
Il s'agit tout simplement de 5 petits typedef bien utiles. Notez les valeurs par défaut. À propos de Category : il s'agit d'un type parmi std::{ input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag,  random_access_iterator_tag }. C'est la matérialisation des concepts souvent employés RandomAccessIterator, ForwardIterator, etc. Libre à vous définir vos propres catégories (en héritant de std::andom_access_iterator_tag par exemple).
 
Il vous reste donc une chose à faire : quand vous créez votre classe d'iterator, héritez publiquement de std::iterator<ce qui va bien>. Ça ne coûte rien :) (Voir plus bas pour un exemple.)
 
 
C'est quoi les traits
Les iterators sont une ~abstraction des pointeurs. En C++, ils servent à matérialiser le concept de séquence. Dans une application, on a souvent besoin de mélanger iterators et pointeurs classiques. Le problème, c'est que vous coder un algorithme générique, mais les petites différences entre pointeurs et iterators sont assez ennuyeuses à gérer. Et bien les traits sont une solution à ce genre de problème.
 
Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details". B.S.
 
Les traits sont des petits objets qui fournissent des informations à propos d'un autre objet (ou algorithme) pour déterminer la marche à suivre ou des détails d'implémentation. Dans notre cas, nous avons donc besoin d'une classe iterator_traits pour faire la jointure entre pointeur et iterator. Techniquement, les traits se présentent comme une cartographie de type : on fournit un argument template T, et le traits va nous renseigner grâce à une série de typedef.
 
 
Description de std::iterator_traits<>
 
Rien de très compliqué, on veut juste unifier iterators, pointeurs et const-pointeurs. On fournit 5 typedefs, c'est la projection des typedefs de std::iterator.
 
Notez bien les différences sur "pointer" et "reference".
 

Code :
  1. template<typename _Iterator>
  2. struct iterator_traits
  3. {
  4.   typedef typename _Iterator::iterator_category iterator_category;
  5.   typedef typename _Iterator::value_type        value_type;
  6.   typedef typename _Iterator::difference_type   difference_type;
  7.   typedef typename _Iterator::pointer           pointer;
  8.   typedef typename _Iterator::reference         reference;
  9. };
  10. template<typename _Tp>
  11. struct iterator_traits<_Tp*>
  12. {
  13.   typedef random_access_iterator_tag iterator_category;
  14.   typedef _Tp                         value_type;
  15.   typedef ptrdiff_t                   difference_type;
  16.   typedef _Tp*                        pointer;
  17.   typedef _Tp&                        reference;
  18. };
  19. template<typename _Tp>
  20. struct iterator_traits<const _Tp*>
  21. {
  22.   typedef random_access_iterator_tag iterator_category;
  23.   typedef _Tp                         value_type;
  24.   typedef ptrdiff_t                   difference_type;
  25.   typedef const _Tp*                  pointer;
  26.   typedef const _Tp&                  reference;
  27. };


 
 
 
 
L'exemple
 
Objectifs :  
- définir une fonction distance(first, last) qui calcule la distance entre 2 itérateurs. Cette fonction est semblable à std:: distance définie dans <iterator>
- améliorer cette fonction si nécessaire.
 
 
Pour la première partie, c'est relativement facile. Une fois qu'on a passé les longs noms de types, c'est de la tarte. Pas de quoi s'affoler. Ici, on utilise les traits pour obtenir "automatiquement" le type de retour adéquate représentant une différence entre 2 iterator.
 
 

Code :
  1. namespace My
  2. {
  3.   template<typename InputIterator>
  4.   typename std::iterator_traits<InputIterator>::difference_type
  5.   distance(InputIterator first, InputIterator last)
  6.   {
  7.     typename std::iterator_traits<InputIterator>::difference_type n = 0;
  8.     while (first != last)
  9.       {
  10. ++first;
  11. ++n;
  12.       }
  13.     return n;
  14.   }
  15. }


 
Maintenant, bricolons-nous 2 classes d'iterator pour voir ce que ça donne :
- un ForwardIterator (typiquement un iterator de liste)
- un RandomAccessIterator (un iterator de vecteur)
sont 2 classes simples qui porte chacune une valeur.
 
NB : j'ai "tassé" tout le code dans définition pour faire au plus cours possible, et je n'ai défini que les fonctions membres nécessaires à l'exemple. Par exemple pour la classe Forward, il manque un operator++(int).
 
 

Code :
  1. namespace My
  2. {
  3.   template<typename T>
  4.   struct Forward
  5.     : std::iterator<std::forward_iterator_tag, T>
  6.   {
  7.     Forward(T v) : value(v)
  8.     { }
  9.     bool operator!=(const Forward &other) const
  10.     {
  11.       return this->value != other.value;
  12.     }
  13.     Forward& operator++()
  14.     {
  15.       std::cout << "++Forward\n";
  16.       this->value++;
  17.       return *this;
  18.     }
  19.     private:
  20.       T value;
  21.   };
  22.   template<typename T>
  23.   struct Random
  24.     : std::iterator<std::random_access_iterator_tag, T>
  25.   {
  26.     Random(T v) : value(v)
  27.     { }
  28.     bool operator!=(const Random &other) const
  29.     {
  30.       return this->value != other.value;
  31.     }
  32.     typename std::iterator<std::random_access_iterator_tag, T>::difference_type
  33.     operator-(const Random &other) const
  34.     {
  35.       return this->value - other.value;
  36.     }
  37.     Random& operator++()
  38.     {
  39.       std::cout << "++Random\n";
  40.       this->value++;
  41.       return *this;
  42.     }
  43.     private:
  44.       T value;
  45.   };
  46. }


 
 
Et voilà un main de test. On utilise des int comme argument pour Forward et Random.
 

Code :
  1. int main()
  2. {
  3.   typedef My::Forward<int> I;
  4.   typedef My::Random<int> R;
  5.   // complexité linéaire : normal
  6.   std::cout << "first <-> last\n"
  7.     << My::distance(I(0), I(3))
  8.     << "\n\n";
  9.   // complexité linéaire : aïe !
  10.   std::cout << "first <-> last\n"
  11.     << My::distance(R(0), R(3))
  12.     << "\n\n";
  13. }


 
 
Très bien ça marche. Maintenant, c'est un peu dommage que le calcul de distance entre 2 RandomAccessIterator soit en temps linéaire. Ça veut dire que si notre iterator est sur un conteneur à accès aléatoire en temps constant, et bien, on perdrait ce bénéfice : on se retrouve à faire le même traitement que si on avait une liste. On parcours élément par élément et on compte. C'est une perte de temps, et c'est bien compliqué. Nous sommes tous relativement habitués à l'arithmétique des pointeurs : c'est quand même plus pratique (et rapide) de pouvoir faire 'p += n' que 'n' fois '++'. Je rappelle qu'un RandomAccessOperator permet des opérations telles que += et -= donc par extension + et -. Il serait donc inutile de faire n iéerations pour obtenir la distance.
std::iterator_traits<> nous permet d'obtenir la catégorie d'un type d'iterator. On va donc exploiter cette information pour prendre une décision lors du calcul de distance.
 

Code :
  1. namespace
  2. {
  3.   template<typename InputIterator, typename Tag>
  4.   struct DistanceWorker
  5.   {
  6.     static typename std::iterator_traits<InputIterator>::difference_type
  7.     distance(InputIterator first, InputIterator last)
  8.     {
  9.       // on utilise la version générique
  10.       return My::distance(first, last);
  11.     }
  12.   };
  13.   template<typename InputIterator>
  14.   struct DistanceWorker<InputIterator, std::random_access_iterator_tag>
  15.   {
  16.     static typename std::iterator_traits<InputIterator>::difference_type
  17.     distance(InputIterator first, InputIterator last)
  18.     {
  19.       return last - first;
  20.     }
  21.   };
  22. }
  23. namespace My
  24. {
  25.   // et hop, un petit sucre syntaxique pour faire en sorte que la déduction de types
  26.   // fasse tout le travail
  27.   template<typename InputIterator>
  28.   typename std::iterator_traits<InputIterator>::difference_type
  29.   distance2(InputIterator first, InputIterator last)
  30.   {
  31.     typedef typename std::iterator_traits<InputIterator>::iterator_category Tag;
  32.     return DistanceWorker<InputIterator, Tag>::distance(first, last);
  33.   }
  34. }


 
C'est gagné !
 
 
Il va de soit qu'une fois que le compilateur est passé la dessus, à grand coup d'inlining, on tombe sur une expression simple. Par exemple, si R est un RandomAccessIterator,  My:: distance2(R(M), R(N)) sera réduit -- à la compilation -- à l'expression (M - N). Et oui, tout ça pour ça :) Si vous trouvez que ça ne vaut pas la peine, la version générique My:: distance vous satisfera. Remarquez juste que si un jour, vous décidez que changez d'algorithme dans un cas particulier (ici, passage d'une boucle à une sous-traction), une simple recompilation suffira.
 
 
----
 
 
Le programme complet
 

Code :
  1. #include <iterator>
  2. #include <cstddef>
  3. #include <iostream>
  4. namespace My
  5. {
  6.   template<typename InputIterator>
  7.   typename std::iterator_traits<InputIterator>::difference_type
  8.   distance(InputIterator first, InputIterator last)
  9.   {
  10.     typename std::iterator_traits<InputIterator>::difference_type n = 0;
  11.     while (first != last)
  12.       {
  13. ++first;
  14. ++n;
  15.       }
  16.     return n;
  17.   }
  18.   template<typename T>
  19.   struct Forward
  20.     : std::iterator<std::forward_iterator_tag, T>
  21.   {
  22.     Forward(T v) : value(v)
  23.     { }
  24.     bool operator!=(const Forward &other) const
  25.     {
  26.       return this->value != other.value;
  27.     }
  28.     Forward& operator++()
  29.     {
  30.       std::cout << "++Forward\n";
  31.       this->value++;
  32.       return *this;
  33.     }
  34.     private:
  35.       T value;
  36.   };
  37.   template<typename T>
  38.   struct Random
  39.     : std::iterator<std::random_access_iterator_tag, T>
  40.   {
  41.     Random(T v) : value(v)
  42.     { }
  43.     bool operator!=(const Random &other) const
  44.     {
  45.       return this->value != other.value;
  46.     }
  47.     typename std::iterator<std::random_access_iterator_tag, T>::difference_type
  48.     operator-(const Random &other) const
  49.     {
  50.       return this->value - other.value;
  51.     }
  52.     Random& operator++()
  53.     {
  54.       std::cout << "++Random\n";
  55.       this->value++;
  56.       return *this;
  57.     }
  58.     private:
  59.       T value;
  60.   };
  61. }
  62. namespace
  63. {
  64.   template<typename InputIterator, typename Tag>
  65.   struct DistanceWorker
  66.   {
  67.     static typename std::iterator_traits<InputIterator>::difference_type
  68.     distance(InputIterator first, InputIterator last)
  69.     {
  70.       return My::distance(first, last);
  71.     }
  72.   };
  73.   template<typename InputIterator>
  74.   struct DistanceWorker<InputIterator, std::random_access_iterator_tag>
  75.   {
  76.     static typename std::iterator_traits<InputIterator>::difference_type
  77.     distance(InputIterator first, InputIterator last)
  78.     {
  79.       return last - first;
  80.     }
  81.   };
  82. }
  83. namespace My
  84. {
  85.   template<typename InputIterator>
  86.   typename std::iterator_traits<InputIterator>::difference_type
  87.   distance2(InputIterator first, InputIterator last)
  88.   {
  89.     typedef typename std::iterator_traits<InputIterator>::iterator_category Tag;
  90.     return DistanceWorker<InputIterator, Tag>::distance(first, last);
  91.   }
  92. }
  93. int main()
  94. {
  95.   typedef My::Forward<int> I;
  96.   typedef My::Random<int> R;
  97.   // complexité linéaire : normal
  98.   std::cout << "first <-> last\n"
  99.     << My::distance(I(0), I(3))
  100.     << "\n\n";
  101.   // complexité linéaire : aïe !
  102.   std::cout << "first <-> last\n"
  103.     << My::distance(R(0), R(3))
  104.     << "\n\n";
  105.   // complexité linéaire : normal
  106.   std::cout << "first <-> last\n"
  107.     << My::distance2(I(0), I(3))
  108.     << "\n\n";
  109.   // complexité 1 !
  110.   std::cout << "first <-> last\n"
  111.     << My::distance2(R(0), R(3))
  112.     << "\n\n";
  113. }


Message édité par Taz le 06-02-2005 à 16:03:43
Reply

Marsh Posté le 31-01-2005 à 21:55:28   

Reply

Marsh Posté le 31-01-2005 à 21:58:39    

Ca a l'air fresh, mais ca sert a quoi ?

Reply

Marsh Posté le 31-01-2005 à 22:06:57    

à gueuler sur les débutants : "va lire tous les autres "Sujets utiles" C++"

Reply

Marsh Posté le 31-01-2005 à 22:16:57    

:o Joce, tu veux pas agrandir cette saleté de fenêtre d'édition, j'en chie grave

Reply

Marsh Posté le 31-01-2005 à 23:14:08    

très instructif...
 
merci!

Reply

Marsh Posté le 31-01-2005 à 23:15:43    

j'ai pas terminé, faudra que je pas que j'oublie de boucler ça

Reply

Marsh Posté le 31-01-2005 à 23:27:18    

Taz a écrit :

:o Joce, tu veux pas agrandir cette saleté de fenêtre d'édition, j'en chie grave


 
hfr enhance [:petrus75]

Reply

Marsh Posté le 01-02-2005 à 12:04:44    

UP.
 
des questions ?

Reply

Marsh Posté le 01-02-2005 à 13:24:30    

voui une question
 

Citation :

Il vous reste donc une chose à faire : quand vous créez votre classe d'iterator, héritez publiquement de std::iterator<ce qui va bien>. Ça ne coûte rien :) (Voir plus bas pour un exemple.)


 
meuh les héritages sont privés dans l'exemple ...

Reply

Marsh Posté le 01-02-2005 à 15:26:55    

Si je ne me trompe pas ils sont publics puisque ce sont des structures et pas des classes...

Reply

Marsh Posté le 01-02-2005 à 15:26:55   

Reply

Marsh Posté le 01-02-2005 à 15:39:04    

au temps pour moi, j'ignorai que le mot clé struct implicitait un héritage public par défaut. Allez je m'autoflagelle d'un coup de stroustrup sur le crane :o

Reply

Marsh Posté le 01-02-2005 à 17:02:26    

++fab a écrit :

Allez je m'autoflagelle d'un coup de stroustrup sur le crane :o


 
Ouch :sweat:

Reply

Marsh Posté le 06-02-2005 à 16:03:59    

terminé

Reply

Marsh Posté le 06-02-2005 à 17:26:16    

un autre exemple de traits dans la bibliothèque standard du C++ : <limits>

Reply

Marsh Posté le 08-02-2005 à 00:01:59    

ça intéresse personne ? c'est trop technique ? l'exemple n'est pas assez concret ? ou quoi ?

Reply

Marsh Posté le 08-02-2005 à 00:02:40    

bin on ecrit pas des iterator tous les jours

Reply

Marsh Posté le 08-02-2005 à 00:06:32    

D'accord, mais bon, c'est pour présenter un concept général. Et je trouves qu'on néglige un peu les iterators : j'ai déjà donné un exexemple d'insert_iterator pour réaliser des insertions dans un dictionnaire en appliquant un petit traitement : c'est un petit paradigme.
 
J'ai voulu faire un peu concret, en m'appuyant sur les iterators, parler crument des traits, ça aurait encore plus bidesque

Reply

Marsh Posté le 08-02-2005 à 00:28:23    

Taz a écrit :

ça intéresse personne ? c'est trop technique ? l'exemple n'est pas assez concret ? ou quoi ?


 
Si c'est intéressant, mais peut etre que tout les gens intéressés ont lu le BS, et l'exemple qui est à peu près le meme  :p  
En fait, je pense que c'est un peu trop concret. Ce qui serait intéressant, ce serait de généraliser la bonne utilisation des traits dans un contexte plus général. Dans quels circonstances réaliser une conception qui pourrait efficacement tirer profit de l'utilisation des traits ? Je sais pas si je suis clair ...
 

Citation :

J'ai voulu faire un peu concret, en m'appuyant sur les iterators, parler crument des traits, ça aurait encore plus bidesque


meuh non !

Reply

Marsh Posté le 08-02-2005 à 12:06:29    

ca m'interresse beaucoup aussi, jme plonge dedans ce soir
 
merci beaucoup :jap:


---------------
-( BlackGoddess )-
Reply

Marsh Posté le 08-02-2005 à 13:35:53    

chrisbk a écrit :

bin on ecrit pas des iterator tous les jours


 
on devrait :o

Reply

Marsh Posté le 08-02-2005 à 13:40:48    

Taz a écrit :

ça intéresse personne ? c'est trop technique ? l'exemple n'est pas assez concret ? ou quoi ?


t'inquiète pas, j'ai mis tous tes topics C++ en favoris, je m'y réfère souvent ! :jap:
(récemment j'ai relu ton topic sur la spécialisation des templates, j'espérais pouvoir l'utiliser pour mon plugin Winamp mais je me suis aperçu que c'était un coup de canon pour tuer une mouche :/


---------------
J'ai un string dans l'array (Paris Hilton)
Reply

Marsh Posté le    

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed