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 :
- template<class Category, class Ty, class Diff = ptrdiff_t
- class Pointer = Ty *, class Reference = Ty&>
- struct iterator {
- typedef Category iterator_category;
- typedef Ty value_type;
- typedef Diff difference_type;
- typedef Pointer pointer;
- typedef Reference reference;
- };
|
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 :
- template<typename _Iterator>
- struct iterator_traits
- {
- typedef typename _Iterator::iterator_category iterator_category;
- typedef typename _Iterator::value_type value_type;
- typedef typename _Iterator::difference_type difference_type;
- typedef typename _Iterator::pointer pointer;
- typedef typename _Iterator::reference reference;
- };
- template<typename _Tp>
- struct iterator_traits<_Tp*>
- {
- typedef random_access_iterator_tag iterator_category;
- typedef _Tp value_type;
- typedef ptrdiff_t difference_type;
- typedef _Tp* pointer;
- typedef _Tp& reference;
- };
- template<typename _Tp>
- struct iterator_traits<const _Tp*>
- {
- typedef random_access_iterator_tag iterator_category;
- typedef _Tp value_type;
- typedef ptrdiff_t difference_type;
- typedef const _Tp* pointer;
- typedef const _Tp& reference;
- };
|
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 :
- namespace My
- {
- template<typename InputIterator>
- typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- typename std::iterator_traits<InputIterator>::difference_type n = 0;
- while (first != last)
- {
- ++first;
- ++n;
- }
- return n;
- }
- }
|
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 :
- namespace My
- {
- template<typename T>
- struct Forward
- : std::iterator<std::forward_iterator_tag, T>
- {
- Forward(T v) : value(v)
- { }
- bool operator!=(const Forward &other) const
- {
- return this->value != other.value;
- }
- Forward& operator++()
- {
- std::cout << "++Forward\n";
- this->value++;
- return *this;
- }
- private:
- T value;
- };
- template<typename T>
- struct Random
- : std::iterator<std::random_access_iterator_tag, T>
- {
- Random(T v) : value(v)
- { }
- bool operator!=(const Random &other) const
- {
- return this->value != other.value;
- }
- typename std::iterator<std::random_access_iterator_tag, T>::difference_type
- operator-(const Random &other) const
- {
- return this->value - other.value;
- }
- Random& operator++()
- {
- std::cout << "++Random\n";
- this->value++;
- return *this;
- }
- private:
- T value;
- };
- }
|
Et voilà un main de test. On utilise des int comme argument pour Forward et Random.
Code :
- int main()
- {
- typedef My::Forward<int> I;
- typedef My::Random<int> R;
- // complexité linéaire : normal
- std::cout << "first <-> last\n"
- << My::distance(I(0), I(3))
- << "\n\n";
- // complexité linéaire : aïe !
- std::cout << "first <-> last\n"
- << My::distance(R(0), R(3))
- << "\n\n";
- }
|
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 :
- namespace
- {
- template<typename InputIterator, typename Tag>
- struct DistanceWorker
- {
- static typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- // on utilise la version générique
- return My::distance(first, last);
- }
- };
- template<typename InputIterator>
- struct DistanceWorker<InputIterator, std::random_access_iterator_tag>
- {
- static typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- return last - first;
- }
- };
- }
- namespace My
- {
- // et hop, un petit sucre syntaxique pour faire en sorte que la déduction de types
- // fasse tout le travail
- template<typename InputIterator>
- typename std::iterator_traits<InputIterator>::difference_type
- distance2(InputIterator first, InputIterator last)
- {
- typedef typename std::iterator_traits<InputIterator>::iterator_category Tag;
- return DistanceWorker<InputIterator, Tag>::distance(first, last);
- }
- }
|
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 :
- #include <iterator>
- #include <cstddef>
- #include <iostream>
- namespace My
- {
- template<typename InputIterator>
- typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- typename std::iterator_traits<InputIterator>::difference_type n = 0;
- while (first != last)
- {
- ++first;
- ++n;
- }
- return n;
- }
- template<typename T>
- struct Forward
- : std::iterator<std::forward_iterator_tag, T>
- {
- Forward(T v) : value(v)
- { }
- bool operator!=(const Forward &other) const
- {
- return this->value != other.value;
- }
- Forward& operator++()
- {
- std::cout << "++Forward\n";
- this->value++;
- return *this;
- }
- private:
- T value;
- };
- template<typename T>
- struct Random
- : std::iterator<std::random_access_iterator_tag, T>
- {
- Random(T v) : value(v)
- { }
- bool operator!=(const Random &other) const
- {
- return this->value != other.value;
- }
- typename std::iterator<std::random_access_iterator_tag, T>::difference_type
- operator-(const Random &other) const
- {
- return this->value - other.value;
- }
- Random& operator++()
- {
- std::cout << "++Random\n";
- this->value++;
- return *this;
- }
- private:
- T value;
- };
- }
- namespace
- {
- template<typename InputIterator, typename Tag>
- struct DistanceWorker
- {
- static typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- return My::distance(first, last);
- }
- };
- template<typename InputIterator>
- struct DistanceWorker<InputIterator, std::random_access_iterator_tag>
- {
- static typename std::iterator_traits<InputIterator>::difference_type
- distance(InputIterator first, InputIterator last)
- {
- return last - first;
- }
- };
- }
- namespace My
- {
- template<typename InputIterator>
- typename std::iterator_traits<InputIterator>::difference_type
- distance2(InputIterator first, InputIterator last)
- {
- typedef typename std::iterator_traits<InputIterator>::iterator_category Tag;
- return DistanceWorker<InputIterator, Tag>::distance(first, last);
- }
- }
- int main()
- {
- typedef My::Forward<int> I;
- typedef My::Random<int> R;
- // complexité linéaire : normal
- std::cout << "first <-> last\n"
- << My::distance(I(0), I(3))
- << "\n\n";
- // complexité linéaire : aïe !
- std::cout << "first <-> last\n"
- << My::distance(R(0), R(3))
- << "\n\n";
- // complexité linéaire : normal
- std::cout << "first <-> last\n"
- << My::distance2(I(0), I(3))
- << "\n\n";
- // complexité 1 !
- std::cout << "first <-> last\n"
- << My::distance2(R(0), R(3))
- << "\n\n";
- }
|
Message édité par Taz le 06-02-2005 à 16:03:43