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