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