Itérateurs et Algorithmes
Transcription
Itérateurs et Algorithmes
MIAGE III — Année 2003/2004 STL, itérarteurs et algorithmes (merci à Lucas Bordeaux) Itérateurs Algorithmes C++ – p.1/36 Conteneurs & itérateurs Une opération extrêmement fréquente sur un conteneur est le parcours des éléments qu’il renferme. Comment le mettre en œuvre? Un accès par operator[] n’est pas satisfaisant pour tous les conteneurs (pour une liste, l’accès au i-ème élément se fait en temps linéaire: temps de parcours quadratique); Pour certains conteneurs (liste), on aimerait avoir un pointeur sur un noeud. Or il n’est pas question d’autoriser cela: les noeuds doivent être des données privées! La solution: associer à chaque classe de conteneur une classe amie d’itérateurs. C++ – p.2/36 Conteneurs & itérateurs En STL, à chaque conteneur Stock contenant des objets de type T sont associés deux types d’itérateurs: Stock::iterator vu comme T* Stock::const_iterator vu comme const T* Les conteneurs présentent les méthodes : iterator begin(); const_iterator begin() const; iterator end(); const_iterator end() const; permettant, respectivement, d’initialiser l’itération et de tester la fin d’itération. C++ – p.3/36 Conteneurs & itérateurs Les itérateurs STL possèdent donc les opérations ++ (passer au suivant), ==, * et -> (déréférencement). Forme typique d’une boucle: void afficher (const List<int>& l) { typedef List<int>::const_iterator CIT; for (CIT it = l.begin(); it != l.end(); it++) cout << *it ; } Notez que end() est un itérateur incorrect (le déréférencer serait dangereux). Il existe aussi (parfois) des itérateurs backward (accessibles par rbegin(), rend()), et aléatoires; Les itérateurs désignent des positions et peuvent servir de paramètres à certaines méthodes (insert). C++ – p.4/36 Conteneurs & itérateurs Un exemple (partiel): itérateur de Liste: template <class Content> struct List<Content>::iterator { Content& operator* () const { return ptr->cont; } iterator& operator++ () { // prefix version ptr = ptr->next; return *this; } // ... private : // data Node* ptr; }; C++ – p.5/36 Conteneurs & itérateurs vector<int> vect (10000); typedef vector<int>::const_iterator IT; for (int i = 0; i <= 10000; i ++) for (IT it = vect.begin(); it != vect.end(); it++) { // ... } javac (-0) g++ (-0) Tableaux de base int[i] 10.5 0.41user (V/v)ector 50.9+ 0.41user Les secrets: absence d’appels virtuels, utilisation d’un type pointeur classique (!), inline. C++ – p.6/36 Conteneurs & Itérateurs Exemple de définition d’un itérateur pour la classe STL list (extrait de stl list.h) template <class _T> struct _List_node { typedef void* _Void_pointer; _Void_pointer _M_next; _Void_pointer _M_prev; _T _M_data; }; template<class _T, class _Ref, class _Ptr> struct _List_iterator { typedef _List_iterator<_T,_T&,_T*> iterator; typedef _List_iterator<_T,const _T&,const _T*> const_iterator; typedef _List_iterator<_T,_Ref,_Ptr> _Self; typedef typedef typedef typedef typedef _T value_type; _Ptr pointer; _Ref reference; _List_node<_T> _Node; size_t size_type; _Node* _M_node; _List_iterator(_Node* __x) : _M_node(__x) {} _List_iterator() {} _List_iterator(const iterator& __x) : _M_node(__x._M_node) {} bool operator==(const _Self& __x) const { return _M_node == __x._M_node; } bool operator!=(const _Self& __x) const { return _M_node != __x._M_node; } ... C++ – p.7/36 Conteneurs & Itérateurs reference operator*() const { return (*_M_node)._M_data; } pointer operator->() const { return &(operator*()); } _Self& operator++() { _M_node = (_Node*)(_M_node->_M_next); return *this; } _Self& operator--() { _M_node = (_Node*)(_M_node->_M_prev); return *this; } }; ... template <class _T, class _Alloc = __STL_DEFAULT_ALLOCATOR(_T) > class list : protected _List_base<_T, _Alloc> { public: typedef _List_iterator<_T,_T&,_T*> iterator; typedef _List_iterator<_T,const _T&,const _T*> const_iterator; iterator begin() iterator end() { return (_Node*)(_M_node->_M_next); } { return _M_node; } bool empty() const { return _M_node->_M_next == _M_node; } reference front() { return *begin(); } const_reference front() const { return *begin(); } reference back() { return *(--end()); } const_reference back() const { return *(--end()); } } C++ – p.8/36 Adaptateurs Comment mettre en œuvre une pile? template <class Type> class Stack; Quel conteneur? un vecteur? une liste? Pourquoi fixer le choix? template <class Container> class Stack; Stack <deque<int> >; // lourd! En fait, utiliser un argument par défaut: template <class T, class C = deque<T> > class Stack{ // ... private : C conteneur; }; C++ – p.9/36 Présupposés sur les paramètres Rien ne garantit a priori que n’importe quelle classe puisse servir de paramètre à un template (ex: Matrix<Animal> ???): En général: construction par défaut et affectation sont nécessaires pour un conteneur; Certaines classes font d’autres présupposés: stack suppose que son deuxième paramètre possède les opérations push_front, etc.; Ce problème est plus général: certaines classes (arbres binaires équilibrés) utilisent une comparaison (conteneurs ordonnées), une matrice suppose l’existence d’une addition et d’une multiplication, etc. C++ – p.10/36 Présupposés sur les paramètres La classe passée en paramètre d’instanciation d’un template doit posséder toutes les fonctions utilisées! Exemple: la méthode operator== est définie pour de nombreux conteneurs, mais si elle est utilisée, il faudra que les objets contenus soient eux-même comparables et possèdent cet opérateur. Si le paramètre n’est pas satisfaisant, le compilateur émettra une erreur ... probablement illisible. Il existe des combines pour exprimer les contraintes sur les paramètres de template. Les bonnes librairies les mettent en œuvre. Mais ces combines sont tordues! C++ – p.11/36 généricité ou polymorphisme? Problème 1 — On souhaite que les fonctions d’usage très général du type: void display_all (const list<int>& l, std::ostream& str) { typedef list<int>::const_iterator IT; IT first = l.begin(); IT stop = l.end(); for (IT it = first; it != stop; it++) str << *it << ’ ’; } ... fonctionnent indépendamment des types réels du conteneur, du contenu, et de l’itérateur. C++ – p.12/36 généricité ou polymorphisme? Première solution: passer par l’héritage: Classe d’itérateur abstraite (méthodes virtuelles pures: ++, *, etc.); Classe de conteneur abstraite (méthode virtuelle: begin(), renvoie le type concret de l’itérateur). Frustrant car: Lors de l’appel à display_all avec un paramètre list<int>, les types de conteneur et d’itérateur sont connus; Pourtant, l’usage des méthodes virtuelles risque de ruiner les opportunités d’optimisation du code. C++ – p.13/36 généricité ou polymorphisme? template <class Container> void display_all (const Container& l, std::ostream& str) { // D’ou l’interet des typedef dans les classes !! // Mais attention au "typename" ... typedef typename Container::const_iterator IT; IT first = l.begin(); IT stop = l.end(); for (IT it = first; it != stop; it++) str << *it << ’ ’; } C++ – p.14/36 généricité ou polymorphisme? Les conteneurs sont un cas particulier d’un dilemme classique entre l’utilisation de l’héritage et de la généricité. Ce dilemme se présente en fait en de nombreuses occasions. 1. L’héritage et le polymorphisme doivent être utilisés lorsque le sens d’une action (méthode) dépend du type de l’objet à l’exécution; 2. Si les paramètres d’un algorithme/d’une classe sont non dynamiques, connus à la compilation, il est préférable d’utiliser un template; 3. Si votre application n’a pas de gros besoin en efficacité, ignorez le conseil (2) et faites ce qui vous parait le plus souple. C++ – p.15/36 Un exemple: l’égalité template <class InputIter1, class InputIter2> inline bool equal(InputIter1 first1, InputIter1 last1, InputIter2 first2) { for ( ; first1 != last1; ++first1, ++first2) if (!(*first1 == *first2)) return false; return true; } template <class Tp, class Alloc> inline bool operator== (const vector<Tp, Alloc>& x, const vector<Tp, Alloc>& y) { return x.size() == y.size() && equal(x.begin(), x.end(), y.begin()); } C++ – p.16/36 Un exemple: le tri La plupart des algos STL sont paramétrés par des itérateurs (début et fin, ou autres). #include <algorithm> using std::sort; using std::vector; // random_access_iterator vector<double> v; // ... sort (v.begin(), v.end()); ]éléments C/sdtio/qsort C++/iostream/sort 500 000 5 000 000 2.5 27.4 5.1 126.6 (source: Stroustrup, OCM 2002) C++ – p.17/36 Quelques algos simples de la STL #include <algorithm> int main () // exemples : count et find { using namespace std; list<int> v; // ... cout << count (v.begin(), v.end(), 7); // on remplace la premiere occurrence de 7 par 12: list<int>::iterator i = find(v.begin(), v.end(), 7); if (i != v.end()) *i = 12; cout << count (v.begin(), v.end(), 7); } C++ – p.18/36 Paramétrer un algo par une fonction Problème 2 — Les algorithmes génériques précédents remplacent des cas simples de boucle; ils sont paramétrés seulement par le conteneur sur lequel ils s’appliquent. Comment paramétrer un algorithme par une action à appliquer à chaque élément du conteneur? // solution 1 : le pointeur sur fonction void afficher (int i); vector<int> v; my_for_each (v.begin(), v.end(), afficher); Solution 1: un pointeur sur fonction... C++ – p.19/36 Objets-fonctions Oui, mais comment faire des calculs plus compliqués, par exemple une somme? template <class T> struct Cumulate { Cumulate () : val (0) {} void operator()(T t) {val += t;} int val; }; Cumulate<int> sum; for_each (v.begin(), v.end(), sum); // presque sum = for_each (v.begin(),v.end(),sum); // oui On a besoin d’une "fonction" possédant un état interne, c-àd. d’un objet fonction. C++ – p.20/36 Paramétrer un algo par une fonction En fait, ce problème est général. De nombreux algorithmes doivent par exemple être paramétrés par un prédicat booléen: find recherche une valeur, on aimerait l’équivalent avec une condition passée en paramètre; sort trie dans l’ordre croissant, on aimerait une plus grande liberté, equal fait une comparaison par paires basée sur l’égalité; pourquoi pas une autre fonction de comparaison? sort prend un 3ème paramètre optionnel, find_if et mismatch sont des alternatives à find et equal. C++ – p.21/36 Objets-fonctions Deux façons de faire un tri en ordre décroissant: inline bool geq (int x, int y) { return x >= y; } struct geq_pred { bool operator () (int x, int y) const { return x >= y; } }; sort (v.begin(), v.end(), geq); sort (v.begin(), v.end(), geq_pred()); C++ – p.22/36 Un exemple signé B. Stroustrup int n; while (cin >> n) vi.push_back (n); sort (vi.begin(), vi.end()); string s; while (cin >> s) vs.push_back (s); sort (vs.begin(), vs.end()); template <class T> void read_and_sort (vector<T>& v) { T t; while (cin >> t) v.push_back(t); sort (v.begin(), v.end()); } C++ – p.23/36 Un exemple signé B. Stroustrup template <class Tp> struct less { bool operator()(const Tp& x, const Tp& y) const { return x < y; } }; template <class T, class Cmp> void read_and_sort (vector<T>& v, Cmp c = less<T>()) { T t; while (cin >> t) v.push_back(t); sort (v.begin(), v.end(), c); } C++ – p.24/36 Objets fonctions Problème 3 — Suis-je vraiment forcé d’écrire ces objets fonctions pour paramétrer mon code? Dans les cas compliqués, oui; Un grand nombre de cas standards est géré par la STL: Des prédicats sont définis en standard (less, equal_to, greater_equal, etc.); D’autres templates existent pour adapter ces prédicats de base à vos besoins... C++ – p.25/36 Objets fonctions namespace std { template <class _Arg, class _Res> struct unary_function { typedef _Arg argument_type; typedef _Res result_type; }; template <class _Arg1, class _Arg2, class _Res> struct binary_function { typedef _Arg1 first_argument_type; typedef _Arg2 second_argument_type; typedef _Res result_type; }; //... C++ – p.26/36 Objets fonctions template <class T> struct plus : public binary_function<T,T,T> { T operator()(const T& x, const T& y) const { return x + y; } }; template <class T> struct less : public binary_function<T,T,bool> { bool operator()(const T& x, const T& y) const { return x < y; } }; C++ – p.27/36 Objets-fonctions Il existe des objets-fonctions pour l’arithmétique (plus, minus, etc.). Inclure <functional>: vector<int> V1(N); vector<int> V2(N); vector<int> V3(N); iota(V1.begin(), V1.end(), 1); fill(V2.begin(), V2.end(), 75); assert(V2.size() >= V1.size()); assert(V3.size() >= V1.size()); transform(V1.begin(), V1.end(), V2.begin(), V3.begin(), plus<int>()); C++ – p.28/36 Objets-fonctions J’ai une classe leq, un algorithme find, et un conteneur v (un vecteur d’entiers). Je voudrais les valeurs inférieures à une valeur précise: 100. En utilisant un binder c’est à dire une classe, paramétrée par une fonction binaire et une valeur: vector<int>::iterator it = find_if (v.begin(), v.end(), bind2nd (less<int>(), 100)); L’idée se généralise, et permet de combiner des opérations unaires ou binaires, booléennes ou numériques. C++ – p.29/36 Objets-fonctions Problème 4 — Je dispose d’un vecteur de Figuress et je souhaite appeler sur chacune la méthode dessiner. Un indice: la solution de la STL utilise des pointeurs sur fonctions membres... Objet_Graphique o; void (Objet_Graphique::*ptr) () const; ptr = & Objet_Graphique::dessiner; (o.*ptr)(); ptr = & Objet_Graphique::aboyer; (o.*ptr)(); C++ – p.30/36 Spécialisation Probleme 5 — Comment redéfinir une classe paramétrée quand elle peut être optimisée/modifée pour un type particulier? Un bon exemple est donnée par la classe vector<bool>, (re)-définie dans un fichier inclus à l’inclusion de vector: template <typename _Alloc> class vector<bool, _Alloc> (grâce à cette spécialisation, le stockage sur un bit est automatique!) C++ – p.31/36 Spécialisation (Un cas extrême dont vous n’entendrez plus jamais parler de votre vie) template<int N> struct Factorial { enum { value = N * Factorial<N-1>::value }; }; template<> struct Factorial<1> { enum { value = 1 }; }; // Example use const int fact5 = Factorial<5>::value; C++ – p.32/36 Traits Problème 6 — Comment associer de l’info à un type passé en paramètre? Par exemple,les Gros_Objets sont passées en paramètre par référence constante: class Gros_Objet { typedef Gros_Objet val; typedef const val& arg; }; template <class T> struct Conteneur { T::val __copie; void ajouter (T::arg); }; C++ – p.33/36 Traits Cette solution ne marche pas pour les types de base! template <class Val, class CstArg> // bah class Conteneur; Conteneur <int, const int&> l; La solution: template <class T> struct Content_Traits{}; template <> struct Content_Traits<int> { typedef int val; /* etc. */ }; template <class T> struct Conteneur { typename traits<T>::val __copie; // ... }; C++ – p.34/36 Traits "Un Trait est utilisé à la place de paramètres de templates. En tant que classe, il aggrège les types, constantes [et fonctions statiques] utiles; en tant que template, il permet un niveau d’indirection supplémentaire." Un exemple simple: template<typename _Tp> struct __is_void { enum {_M_type = 0}; }; template<> struct __is_void<void> { enum {_M_type = 1}; }; C++ – p.35/36 Membres templates Problème 7 — On a vu comment un algorithme peut d’appliquer à un type quelconque de conteneur. Mais comment appliquer une méthode de classe à un conteneur quelconque? Par exemple, on aimerait écrire: list<int> l; // ... vector<int> v (l.begin(), l.end()); En fait, on peut, car il est possible de rendre template une méthode de classe (y-compris un constructeur!); Dans ce cas, il est évidemment interdit de définir la méthode comme virtuelle. C++ – p.36/36