Généricité et STL

Transcription

Généricité et STL
MIAGE III — Année 2003/2004
Généricité en C++
Fonctions et méthodes génériques
Classes génériques
Présentation de la STL
C++ – p.1/39
Introduction
avec l’héritage, il est possible de réaliser des
structures de données indépendantes des classes
réelles des objets stockés
cependant ce mécanisme est lourd (obligation de
dériver d’un classe de base, pour des objets qui ne
partagent aucune affinité)
le contrôle sur les types est affaibli par l’héritage
(structures de données d’objets hétérogènes)
Le C++ introduit les notions de classes et méthodes
génériques
C++ – p.2/39
Généricité
Le langage C++ permet de déclarer des fonctions ou
classes génériques (inspiré de l’ADA).
ces fonctions ou classes permettent de travailler sur
des objets dits génériques, c’est à dire de type
quelconque.
on parle aussi de patrons de classes ou patrons de
méthodes ou encore de templates.
le code d’une méthode ou classe générique n’est
instancié qu’à l’utilisation des types réels (on parle
d’instanciation des templates).
C++ – p.3/39
Fonctions génériques
Déclaration et utilisation :
template <class T>
T Min(T x, T y) {
return x < y ? x : y;
}
...
int x = Min(3,4);
double y = Min(5.0, 6.0);
Polynome p = Min(p1, p2);
// ou p1 et p2 sont deux polynomes
// et l’opérateur < compare leur degré
la fonction Min est donc définie pour toute classe qui possède un
opérateur de comparaison < et un constructeur par défaut (ici le
passage de paramètres se fait par valeur).
le compilateur déterminera le bon opérateur à utiliser en fonction du
type des objets.
une fonction template peut être déclarée amie d’une classe.
C++ – p.4/39
Classes génériques
Déclaration et utilisation :
template <class T>
class Vecteur {
public :
Vecteur (int n=10);
// alloue dynamiquement un tableau
~Vecteur();
// détruit le tableau
T& operator[](int i);
// retourne une ref. sur le i\‘eme objet
T operator[](int i) const;
// retourne une copie du i\‘eme objet
private:
T *_elems;
int _taille;
};
...
Vecteur<int> vi;
v[0] = 5;
v[1] = 3;
Vecteur<double> vd;
vd[5] = 3.0;
vd[5] += 3.0;
Vecteur<Polynome> vp;
vd[0] = p1;
vd[1] = p2;
p3 = vd[0];
// un tableau d’objets de type T
// en ayant déclaré Polynome p1;
// et Polynome p2;
// et Polynome p3;
C++ – p.5/39
Classes génériques : définition et déclaration
si les déclarations de méthodes d’une classe template
ne sont pas faites dans la définition de la classe, elles
doivent déclarées avec le mot clé template :
défintion (dans la classe)
template <class T>
class Vecteur {
...
T& operator[](int i);
};
déclaration (en dehors de la classe) :
template <class T>
T& Vecteur<T>::operator[](int i) {
return _elems[i];
}
C++ – p.6/39
Méthodes template
Mis à part les destructeurs, les méthodes d’une classe
peuvent être template de la même manière que les
fonctions.
// déclaration
class A {
public :
template <class T>
void toto(T x);
};
// définition
template <class T>
void A::toto(T x) {
...
}
// utilisation
A a;
A.toto<double>(3.0); // appel explicite à l’instance <double> de la méthode toto
a.toto<>(5);
// l’instance est déterminée par le type du paramètre
a.toto("Hello");
// idem ...
C++ – p.7/39
Méthode template d’une classe template
Il est évidemment possible de déclarer des méthodes
templates dans une classe template.
template<class T>
class Chaine
{
public:
template<class T2> int compare(const T2 &); // Fonction membre template définie
// à l’extérieur de la classe template :
template<class T2>
Chaine(const Chaine<T2> &s)
{
// ...
}
// Fonction membre template définie
// à l’intérieur de la classe template :
};
// À l’extérieur de la classe template, on doit donner
// les déclarations template pour la classe
// et pour la fonction membre template :
template<class T> template<class T2>
int Chaine<T>::compare(const T2 &s)
{
// ...
}
C++ – p.8/39
Méthode membre et méthode virtuelle
Une méthode virtuelle ne peut pas être template. En cas de
méthodes de template et virtuelle ayant des signatures
identiques, la méthode template ne redéfinit pas la fonction
virtuelle. Il est cependant possible de s’en sortir :
class B
{
virtual void f(int);
};
class D : public B
{
template <class T>
void f(T);
// Cette fonction ne redéfinit pas B::f(int).
void f(int i)
{
f<>(i);
}
// Cette fonction surcharge B::f(int).
// Appel de la fonction template,
// et non de la méthode elle-même, grace
// à l’utilisation de <>
};
C++ – p.9/39
Les templates par défaut
Il est possible de préciser une classe par défaut pour un
template de classe ou de méthode :
// déclaration
template <class T, class U=int>
class A {
};
// utilisation
A<double> a;
A<double, double> aa;
// la classe est templatée par <double, int>
// la classe est templatée par <double, double>
le mot clé typename remplace parfois le mot clé
class. Dans ce cas précis, l’un ou l’autre peuvent être
utilisés indifféremment.
si un des templates est déclaré par défaut, tous les
suivants doivent l’être.
C++ – p.10/39
Template de template
il est possible d’utiliser une classe template en tant que
type générique :
template <template <class Type> class Class>
la classe est déclarée comme template à l’intérieur
de la fonction template
on parle de paramètre template template
il est possible de lui attribuer une valeur par défaut :
template <class T>
class Tableau {
};
template <class U, class V, template <class T> class C=Tableau>
class Dictionnaire
{
C<U> Clef;
C<V> Valeur;
};
C++ – p.11/39
Déclaration de constantes template
Il est possible de déclarer des paramètres template de
type constante. Dans ce cas, ne pas utiliser le mot clé
class :
template <class T, int x=5>
class Tableau {
...
private:
T _tableau[x];
// construit un tableau à x éléments de type T
};
Un paramètre template de type constante doit
obligatoirement être l’un des types suivants
type intégral : int et ses variations, char et ses
variations, type enuméré
pointeur ou référence d’objet
pointeur ou référence de fonction
pointeur sur membre
C++ – p.12/39
Instanciation des templates
L’instanciation est effectuée une fois que tous les
paramètres templates ont pris une valeur spécifique, valeur
donnée soit par l’utilisateur, soit la valeur par défaut.
Il existe deux formes d’instanciation des templates :
l’instanciation implicite
l’instanciation explicite
La politique des compilateurs est d’instancier le moins
possible. Ansi, dans une classe template, seules les
méthodes réellement appelées seront instanciées.
template <class T>
class A
{
public:
void f(void);
void g(void);
};
...
A<char> a;
// instancie A<char>
a.f();
// instancie A<char>::f()
// mais n’instancie pas A<char>::g()
C++ – p.13/39
Instanciation implicite
Lors d’une instanciation implicite, le compilateur détermine
les types des paramètres et génère le code correspondant
à ces paramètres. Il y aura donc autant d’instances de code
que de types différents utilisés !
template <class T>
T Min(T x, T y) {
return x < y ? x : y;
}
int i = Min(2,3);
// le compilateur génère le code correspondant à T=int
int j = Min(2,3.0); // le compilateur signale l’ambiguité T=int ou T=double
double k = Min<double>(2, 3.0) // le compilateur génère le code correspondant à T=double
//et convertit l’entier 2 en double.
C++ – p.14/39
Instanciation explicite
L’instanciation explicite permet de forcer l’instanciation.
Dans ce cas, il est nécessaire de préciser tous les
paramètres template.
// on force l’instanciation de la fonction template Min
template int Min(int, int)
// on force des instanciations d’une classe template Vecteur
template Vecteur<int>;
template Vecteur<double>;
// on force l’instanciation pour une class dont tous les
// paramètres ont des valeurs par défaut
template Toto<>;
C++ – p.15/39
Problèmes liés aux instances
L’utilisation de classes et méthodes templates posent
certains problèmes dont il faut être conscient.
les templates doivent être entièrement définis afin d’être
implémentés
indispensable de donner les définitions et les déclarations dans un seul et
même fichier entête
les instances des templates sont compilées plusieurs fois
peut induire des temps de compilation importants
certains compilateurs précompilent les entêtes
les instances des templates sont présents plusieurs fois dans
le code objet généré par la compilation.
peut induire un code généré de taille conséquente
certains éditeurs sont modifiés pour n’ajouter qu’une seule instance dans le
code executable.
C++ – p.16/39
Spécialisation des templates
Lorsqu’une fonction ou une classe template est définie, il
est possible de la spécialiser pour un certain jeu de
paramètres template.
La spécialisation est :
totale quand plus aucun des paramètres n’est template
partielle quand seuls quelques paramètres template
ont une valeur fixée
C++ – p.17/39
Spécialisation totale
Pour une spécialisation totale, il est nécessaire de fournir
toutes les valeurs des paramètres template et de précéder
la déclaration par l’instruction template <>
Par exemple, on veut spécialiser la fonction Min pour une
classe spécifique d’objets :
struct Structure
{
int Clef;
void *pData;
};
// Clef permettant de retrouver des données.
// Pointeur sur les données.
template <>
Structure Min<Structure>(Structure s1, Structure s2)
{
if (s1.Clef>s2.Clef)
return s1;
else
return s2;
}
// on remplace le code normalement généré par l’instanciation
// de Min<Structure>
C++ – p.18/39
Spécialisation partielle
La spécialisation partielle permet de définir une implémentation
spécifique d’un ou plusieurs paramètres template (pas tous sinon
c’est une spécialisation totale) et conserver les autres paramètres
template.
// Définition d’une classe template :
template <class T1, class T2, int I>
class A
{
};
// Spécialisation nř1 de la classe :
template <class T, int I>
class A<T, T*, I>
// on force T2 à être de type pointer sur T
{
// et T1 à être de type T
};
// et on en donne une implémentation spécifique
// Spécialisation nř2 de la classe :
template <class T3, class T4, int I>
class A<T3*, T4, I>
// on force T1 à être de type pointeur sur T3
{
// et on en donne une implémentation spécifique
};
// Spécialisation nř3 de la classe :
template <class T>
class A<int, T*, 5>
// on force T1 à être de type int, T2 de type *T
{
// et T3 = 5
};
// et on en donne une implémentation spécifuque
C++ – p.19/39
Spécialisation d’une méthode
Il est possible de spécialiser partiellement ou totalement une
méthode d’une classe template (évite de spécialiser toute la
classe).
template <class T>
class Item
{
public:
Item(T);
void set(T);
T get(void) const;
void print(void) const;
private:
T item;
};
template
template
template
template
<class
<class
<class
<class
T>
T>
T>
T>
Item<T>::Item(T i) { item = i; }
// Constructeur
void Item<T>::set(T i){ item = i; }
// Modificateur
T Item<T>::get(void) const { return item; }
// Accesseur
void Item<T>::print(void) const { cout << item << endl; } // Affichage
// Fonction d’affichage spécialisée explicitement pour le type int *
template <> void Item<int *>::print(void) const
{
cout << *item << endl;
// ici, on ne veut pas afficher l’adresse de l’entier
// mais la valeur de l’entier lui-même
}
C++ – p.20/39
Redéfinition de types
Le mot clé typename peut-être utilisé à la place du mot clé
class dans la déclaration des paramètres template d’un
fonction, méthode ou classe. Il est un cas particulier ou ce
mot clé est utilisé pour déclarer un identificateur de type
inconnu (enfin pas encore connu).
C’est le cas quand on fait référence à un type déclaré dans
une classe paramètre d’un template :
class A {
public:
typedef int Y;
};
template <class T>
class X {
typename T::Y i;
};
// Y est un type défini dans la classe A.
//
//
//
//
La classe template X suppose que le
type générique T définisse un type Y.
on déclare donc un attribut i de type
celui défini dans la class T
...
X<A> x;
C++ – p.21/39
mot clé export
Avec les classes templates, on a vu
qu’il était nécessaire de déclarer entièrement les méthodes
dans un fichier entête
que le fichier est compilé à chaque utilisation d’un template
que différentes instances du même code sont produites
La norme C++ définit cependant le mot clé export qui permet de
donner les définitions dans un fichier entête, et les déclarations
dans un fichier implémentation.
// fichier entête
export template <class T>
void f(T);
// Fonction dont le code n’est pas fourni
// dans les fichiers qui l’utilisent.
// fichier implémentation
export template <class T>
void f(T p) {
...
}
A ce jour, aucun compilateur ne supporte le mot clé export.
C++ – p.22/39
Présentation de la STL
bibliothèque de structures de données et d’algorithmes
essentiellement basée sur l’utilisation des templates
des contraintes fortes garantissent la complexité des
traitements
il est nécessaire d’avoir (très) bien compris les
fonctionnalités (très) avancées du C++ pour
appréhender (à peu près) correctement la STL
C++ – p.23/39
Les conteneurs
La STL offre un ensemble de conteneurs templates
permettant de stocker et de traiter facilement de
l’information. Deux catégories de conteneurs sont offertes :
les séquences
permettent de stocker des éléments de manière
séquentielle
les éléments sont identifiés par leur position dans la
séquence
ne disposent pas d’une fonction de recherche (autre que
parcours séquentiel)
C++ – p.24/39
Les conteneurs
les conteneurs associatifs
permettent d’identifier des éléments au moyen de valeurs
ou clés
la recherche est généralement effectuée par cette clé.
Le choix du conteneur dépend des traitements à effectuer,
du stockage et des performances désirées. Les conteneurs
possèdent tous la notion d’itérateur qui représente un élément de l’ensemble et facilite le parcours du container.
C++ – p.25/39
Les séquences
La STL offre les classes de séquences templates suivantes
les vecteurs vector
les deques deque
les listes list
les piles stack
les files queue
les files de priorités priority queue
C++ – p.26/39
La classe vector<T,Alloc>
La sémantique est très proche de celle des tableaux utilisés
en C.
l’accès à un élément donné est réalisé en coût constant
l’insertion/suppression en milieu de tableau est de complexité linéaire (nécessité de
décaler tous les éléments)
insertion/suppression en fin en temps constant
pas d’insertion/suppression en début
insertion et suppression invalident les itérateurs
le conteneur se charge de la réallocation
Paramètres :
1. le type T des objets à stocker
2. le type Alloc d’allocation à réaliser (possède un allocateur par déafut)
C++ – p.27/39
La classe vector<T,Alloc>
Quelques méthodes :
vector() construit un vecteur vide
vector(size type n) construit un vecteur à n élements
vector(size type n, const T& t) construit un vecteur à n copies de t
iterator begin() retourne un itérateur sur le début du vecteur
iterator end() retrourne un itérateur sur la fin du vecteur
size type size() retourne la taille du vecteur
bool empty() const vrai si le vecteur est vide
T& operator[] (size type n) retourne une référence sur le nième objet (lance
une exception si n > size)
T& operator[] (size type n) const retourne une référence constante sur le
nième objet (lance une exception si n > size)
T& back() retourne une référence sur le dernier élément
T& front() retourne une référence sur le premier élément
void push back(const T& t) ajoute l’objet t en fin de vecteur
void pop back() enleve le dernier élément
C++ – p.28/39
La classe vector<T,Alloc> : exemple
#include <vector>
int main(void)
{
typedef std::vector<int> vi;
vi v(10);
v[2] = 2;
v[5] = 7;
v.resize(11);
v[10] = 5;
v.push_back(13);
// Crée un vecteur de 10 éléments
// Modifie quelques éléments
// Redimensionne le vecteur
// Ajoute un élément à la fin du vecteur
for (int i=0; i<v.size(); ++i) { // Affiche en utilisant l’opérateur []
cout << v[i] << endl;
}
// parcours en utilisant un itérateur
for (vi::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << endl;
// surcharge de l’opérateur * :
}
// retourne l’objet pointé par l’itérateur
return 0;
}
C++ – p.29/39
La classe deque<T,Alloc>
fortement basée sur la class vector<T,Alloc>
l’accès à un élément donné est réalisé en coût constant
l’insertion/suppression en milieu de tableau est de complexité linéaire (nécessité de
décaler tous les éléments)
insertion/suppression en fin en temps constant
insertion/suppression en début en temps constant!
insertion et suppression invalident les itérateurs
le conteneur se charge de la réallocation
#include <deque>
...
deque<int> Q;
Q.push_back(3);
Q.push_front(1);
Q.insert(Q.begin() + 1, 2);
Q[2] = 0;
for (deque<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << endl;
// surcharge de l’opérateur * :
}
// retourne l’objet pointé par l’itérateur
C++ – p.30/39
La classe list<T,Alloc>
implémentée comme une liste doublement chaînée
utiliser slist<T,Alloc> pour une liste
simplement chainée
insertion et suppression en temps constant
insertion et suppression n’invalident pas les itérateurs
#include <list>
list<int> L;
L.push_back(0);
L.push_front(1);
L.insert(++L.begin(), 2);
for (list<int>::iterator it = L.begin(); it != L.end(); it++) {
cout << *it << endl;
}
C++ – p.31/39
Les conteneurs associatifs
Les conteneurs associatifs permettent d’identifier leurs
éléments à l’aide de la valeur d’une clé.
permet une recherche très performante (coût
logarithmique)
l’ordre des éléments n’a pas une importance
primordiale
différents conteneurs associatifs :
qui ne différencient pas la clé et la valeur (on parle
d’ensembles)
qui différencient clé et valeur (on parle d’associations)
à clé unique (1 clé = 1 valeur)
à clés multiples (1 clé = plusieurs valeurs)
C++ – p.32/39
La classe set<Key,Compare,Alloc>
cette classe ne différencie pas la clé de la valeur (c’est
la même entité Key ici).
Key représente l’objet à stocker
il n’existe pas deux éléments identiques dans le
conteneur (conteneur associatif unique)
les éléments sont ordonnés par le fonctor (objet
fonction) Compare
C++ – p.33/39
La classe set<Key,Compare,Alloc>
Quelques méthodes :
set() construit un ensemble vide
set(iterator b, iterator e) construit un ensemble à partir des l’itérateurs b
jusqu’à e
iterator begin() retourne un itérateur sur le début de l’ensemble
iterator end() retrourne un itérateur sur la fin de l’ensemble
size type size() retourne la taille de l’ensemble
bool empty() const vrai si l’ensemble est vide
iterator insert(iterator pos, const T& t) insère t après pos
void erase(iterator pos) supprime l’élément pointé par l’itérateur pos
iterator find(const T& t) retourne un itérateur sur l’élément t dans
l’ensemble ou retourne end() si non trouvé
size type count(const T& t) retourne le nombre d’occurrences de t
opérations ensembliste union, différence, intersection
C++ – p.34/39
set<Key,Compare,Alloc> : exemple
#include <set>
struct ltstr {
bool operator()(const char* s1, const char* s2) const {
return strcmp(s1, s2) < 0;
}};
int main()
{
const int N = 6;
const char* a[N] = {"isomer", "ephemeral", "prosaic",
"nugatory", "artichoke", "serif"};
set<const char*, ltstr> A(a, a + N);
cout << "Set A: ";
for (set<const char *, ltstr>::iterator it = L.begin(); it != L.end(); it++) {
cout << *it << endl;
}
set<const char *, ltstr>::iterator it = A.find("artichoke");
if (it != A.end()) {
A.erase(it);
}
}
C++ – p.35/39
map<Key,Data,Compare,Alloc>
associe des objets de type Data à des clés Key
deux objets Data ne peuvent avoir la même clé (clé
unique)
Compare sert à comparer deux clés
chaque entrée d’une map est une classe
pair<Key,Data>
insertion et suppression n’invalident pas les itérateurs
C++ – p.36/39
map<Key,Data,Compare,Alloc>
#include <map>
struct ltstr {
bool operator()(const char* s1, const char* s2) const {
return strcmp(s1, s2) < 0;
}
};
int main() {
map<const char*, int, ltstr> months;
months["january"] = 31;
months["february"] = 28;
months["march"] = 31;
months["april"] = 30;
months["may"] = 31;
months["june"] = 30;
months["july"] = 31;
months["august"] = 31;
months["september"] = 30;
months["october"] = 31;
months["november"] = 30;
months["december"] = 31;
cout << "june -> " << months["june"] << endl;
map<const char*, int, ltstr>::iterator cur = months.find("june");
map<const char*, int, ltstr>::iterator prev = cur;
map<const char*, int, ltstr>::iterator next = cur;
++next;
--prev;
cout << "Previous (in alphabetical order) is " << (*prev).first << endl;
cout << "Next (in alphabetical order) is " << (*next).first << endl;
}
C++ – p.37/39
multimap<Key,Data,Compare,Alloc>
associe des objets de type Data à des clés Key
deux objets Data peuvent avoir la même clé (clé
multiple)
Compare sert à comparer deux clés
chaque entrée d’une multimap est une classe
pair<Key,Data>
insertion et suppression n’invalident pas les itérateurs
C++ – p.38/39
map<Key,Data,Compare,Alloc>
struct ltstr {
bool operator()(const char* s1, const char* s2) const {
return strcmp(s1, s2) < 0;
}
};
int main()
{
multimap<const char*, int, ltstr> m;
m.insert(pair<const
m.insert(pair<const
m.insert(pair<const
m.insert(pair<const
m.insert(pair<const
m.insert(pair<const
char*
char*
char*
char*
char*
char*
const,
const,
const,
const,
const,
const,
int>("a",
int>("c",
int>("b",
int>("b",
int>("a",
int>("b",
1));
2));
3));
4));
5));
6));
cout << "Number of elements with key a: " << m.count("a") << endl;
cout << "Number of elements with key b: " << m.count("b") << endl;
cout << "Number of elements with key c: " << m.count("c") << endl;
cout << "Elements in m: " << endl;
for (multimap<const char*, int, ltstr>::iterator it = m.begin();
it != m.end();
++it)
cout << " [" << (*it).first << ", " << (*it).second << "]" << endl;
}
C++ – p.39/39