Chapitre 3 : La Généricité et les Templates en C++
Écrire du code indépendant du type sans compromis sur la performance ni la sûreté du typage. Maîtriser les patrons de fonctions, concevoir des conteneurs de données génériques et appréhender la compilation des templates.
Objectifs pédagogiques de la séance
- Comprendre l'intérêt fondamental de la généricité : éviter la duplication de code sans perte de typage statique.
- Créer et instancier des patrons de fonctions (function templates) avec déduction automatique de types.
- Concevoir des patrons de classes (class templates) et implémenter des conteneurs génériques (ex:
Pile<T>). - Manipuler les paramètres non-types de templates (ex: tailles de tableaux fixes à la compilation).
- Comprendre le mécanisme de compilation des templates et la règle de séparation des fichiers (
.hpp/.tpp).
L'Intérêt de la Généricité : Pourquoi et Comment ?
1.1. Le problème de la duplication de code
En C++ standard fortement typé, une fonction ou une structure de données est normalement liée à des types précis.
Imaginez que vous deviez écrire une fonction echanger (swap) ou concevoir une Pile pour des entiers (int), des réels (double) et des chaînes de caractères (std::string) :
❌ Sans Généricité : Duplication Massive
void echanger(int& a, int& b) { /*...*/ } void echanger(double& a, double& b) { /*...*/ } void echanger(std::string& a, std::string& b) { /*...*/ }
Code identique dupliqué N fois. Risque d'erreur de maintenance énorme !
⚠️ L'approche du Langage C : Les pointeurs void*
void echanger(void* a, void* b, size_t taille) { // Manipulation manuelle d'octets avec memcpy }
Perte totale de la sécurité de typage à la compilation, risque élevé de corruption mémoire (Segfault).
1.2. La solution C++ : La Programmation Générique (Templates)
La généricité permet d'écrire un algorithme ou une structure de données en utilisant un ou plusieurs types abstraits (paramètres de type). Le compilateur générera lui-même automatiquement le code machine spécifique pour chaque type utilisé lors de la compilation (mécanisme de monomorphisation).
Un patron (template) est comme un moule à gâteau. Le moule définit la forme exacte (l'algorithme et la structure), mais vous pouvez y couler du chocolat, de la pâte vanille ou de la cire. Le moule en lui-même n'est pas un gâteau mangeable : c'est le plan qui permet de fabriquer des gâteaux concrets à la demande.
1.3. Tableau comparatif des approches
| Approche | Sécurité de Typage | Performance à l'Exécution | Maintenance du Code |
|---|---|---|---|
| Surcharge / Duplication | Excellente (statique) | Optimale | Désastreuse (code copié-collé) |
Pointeurs bruts void* (C) |
Nulle (transtypages dangereux) | Moyenne (appels indirections) | Moyenne et source de bugs |
| Templates C++ (Généricité) | Maximale (vérifiée à la compilation) | Optimale (zéro surcoût / Zero-cost abstraction) | Idéale (écrit une seule fois) |
Les Patrons de Fonctions (Function Templates)
2.1. Déclaration et Syntaxe
Un patron de fonction est précédé du mot-clé template suivi de la liste des paramètres de type entre chevrons <typename T> (ou <class T>) :
#include <iostream> #include <string> // 1. Définition du patron de fonction echanger template <typename T> void echanger(T& a, T& b) { T temp = a; a = b; b = temp; } // 2. Définition du patron de fonction minimum template <typename T> T minimum(const T& a, const T& b) { return (a < b) ? a : b; } int main() { // Utilisation avec des entiers (int) int n1 = 10, n2 = 25; echanger(n1, n2); // Déduction automatique : T = int std::cout << "Min(10, 25) = " << minimum(n1, n2) << std::endl; // Utilisation avec des chaînes (std::string) std::string s1 = "Zèbre", s2 = "Antilope"; echanger(s1, s2); // Déduction automatique : T = std::string std::cout << "Min alphabétique = " << minimum(s1, s2) << std::endl; // Spécification explicite du type si nécessaire double d = minimum<double>(5.5, 12); std::cout << "Min double = " << d << std::endl; return 0; }
2.2. Que fait réellement le compilateur ? L'Instanciation
Lorsque le compilateur rencontre minimum(n1, n2) avec des int, il génère en coulisses le code binaire équivalent à une fonction int minimum(const int&, const int&).
Puis, lorsqu'il rencontre minimum(s1, s2) avec des std::string, il génère une seconde version spécialisée pour std::string.
template <typename T> T minimum(T a, T b)
int minimum(int, int)string minimum(string, string)double minimum(double, double)
2.3. Patrons avec plusieurs paramètres de types
Une fonction peut accepter plusieurs paramètres de types indépendants :
template <typename T1, typename T2> void afficherPaire(const T1& premier, const T2& second) { std::cout << "(" << premier << " : " << second << ")\n"; } // Utilisation : afficherPaire("Note", 18.5); // T1 = const char*, T2 = double
Patrons de Classes et Conteneurs Génériques
L'application reine de la généricité en POO est la création de **conteneurs de données** (tableaux dynamiques, listes, piles, files, arbres, tables de hachage) capables de stocker n'importe quel type d'éléments.
3.1. Exemple simple : La classe générique Paire<T1, T2>
#ifndef PAIRE_HPP #define PAIRE_HPP template <typename T1, typename T2> class Paire { private: T1 m_premier; T2 m_second; public: Paire(const T1& p, const T2& s) : m_premier(p), m_second(s) {} T1 getPremier() const { return m_premier; } T2 getSecond() const { return m_second; } void setPremier(const T1& val) { m_premier = val; } void setSecond(const T2& val) { m_second = val; } }; #endif // PAIRE_HPP
3.2. Conteneur dynamique complet : Implémentation d'une Pile<T> (Stack)
Voici l'implémentation robuste d'un conteneur générique Pile<T> (LIFO - Last In First Out) avec allocation dynamique et respect strict de la **Règle des Trois** :
#ifndef PILE_HPP #define PILE_HPP #include <iostream> #include <stdexcept> template <typename T> class Pile { private: T* m_elements; // Tableau dynamique d'éléments de type T size_t m_capacite; // Capacité maximale allouée size_t m_sommet; // Nombre d'éléments empilés void redimensionner(size_t nouvelleCapacite) { T* nouv = new T[nouvelleCapacite]; for (size_t i = 0; i < m_sommet; ++i) { nouv[i] = m_elements[i]; } delete[] m_elements; m_elements = nouv; m_capacite = nouvelleCapacite; } public: // 1. Constructeur usuel explicit Pile(size_t capaciteInitiale = 4) : m_elements(new T[capaciteInitiale]), m_capacite(capaciteInitiale), m_sommet(0) {} // 2. Destructeur ~Pile() { delete[] m_elements; } // 3. Constructeur par recopie (Copie profonde générique) Pile(const Pile<T>& autre) : m_elements(new T[autre.m_capacite]), m_capacite(autre.m_capacite), m_sommet(autre.m_sommet) { for (size_t i = 0; i < m_sommet; ++i) { m_elements[i] = autre.m_elements[i]; } } // 4. Opérateur d'affectation Pile<T>& operator=(const Pile<T>& autre) { if (this != &autre) { delete[] m_elements; m_capacite = autre.m_capacite; m_sommet = autre.m_sommet; m_elements = new T[m_capacite]; for (size_t i = 0; i < m_sommet; ++i) { m_elements[i] = autre.m_elements[i]; } } return *this; } // Méthodes de manipulation de la pile void empiler(const T& element) { if (m_sommet == m_capacite) { redimensionner(m_capacite * 2); } m_elements[m_sommet++] = element; } T depiler() { if (estVide()) { throw std::underflow_error("Impossible de dépiler : la pile est vide !"); } return m_elements[--m_sommet]; } const T& sommet() const { if (estVide()) { throw std::underflow_error("Pile vide !"); } return m_elements[m_sommet - 1]; } bool estVide() const { return m_sommet == 0; } size_t taille() const { return m_sommet; } }; #endif // PILE_HPP
3.3. Paramètres Non-Types (Non-Type Template Parameters)
Un template peut également recevoir des valeurs constantes connues à la compilation (comme un entier ou une taille), ce qui permet d'allouer des conteneurs sans passer par la mémoire dynamique sur le tas :
#ifndef TABLEAUFIXE_HPP #define TABLEAUFIXE_HPP #include <cstddef> #include <stdexcept> template <typename T, size_t N> class TableauFixe { private: T m_donnees[N]; // Tableau statique alloué sur la pile (stack) public: size_t capacite() const { return N; } size_t taille() const { return N; } T& operator[](size_t index) { return m_donnees[index]; } const T& operator[](size_t index) const { return m_donnees[index]; } T& at(size_t index) { if (index >= N) { throw std::out_of_range("Index hors limites !"); } return m_donnees[index]; } }; #endif // TABLEAUFIXE_HPP
Organisation des Fichiers et Pièges Classiques
Contrairement aux classes ordinaires où les déclarations vont dans le .hpp et l'implémentation dans un .cpp compilé séparément, l'implémentation complète d'un template DOIT être disponible dans le fichier d'en-tête (.hpp ou .tpp inclus dans le header).
Pourquoi ? Le compilateur a besoin du code source complet du template pour pouvoir l'instancier au moment précis où un fichier utilisateur écrit Pile<int>. Si le corps est dans un Pile.cpp séparé, l'éditeur de liens échouera avec une erreur « undefined reference to Pile<int> ».
📌 Exigences sur le type T
Un template impose implicitement des contraintes sur le type T :
- Si le template utilise
a < b, le typeTdoit obligatoirement définir l'operator<. - Si le conteneur alloue
new T[N], le typeTdoit posséder un constructeur par défaut.
💡 Bonne Pratique : Découpage .hpp / .tpp
Pour garder des headers lisibles :
// Dans Pile.hpp template <typename T> class Pile { /* déclarations */ }; #include "Pile.tpp" // Implémentation
Synthèse & Auto-évaluation
5.1. Fiche Mémo Récapitulative
| Notion | Syntaxe Type | Point Clé |
|---|---|---|
| Patron de fonction | template <typename T> T f(T a); |
Le type peut être déduit automatiquement à l'appel : f(x). |
| Patron de classe | template <typename T> class C { ... }; |
Le type est spécifié entre chevrons à l'instanciation : C<int> obj;. |
| Paramètre non-type | template <typename T, size_t N> |
N est une constante connue à la compilation. |
| Emplacement du code | Dans le fichier .hpp / .tpp |
Nécessaire pour permettre l'instanciation à la compilation dans chaque unité de traduction. |
5.2. QCM de Réflexion (Niveau L3)
Vector<T> dans un fichier Vector.cpp compilé séparément, sans inclusion dans le .hpp ?
main.cpp inclut Vector.hpp et instancie Vector<int>, le compilateur ne trouve pas le corps des méthodes et émet des symboles non résolus. Comme Vector.cpp ne sait pas pour quels types Vector sera utilisé, il ne génère aucune version concrète : le linker échoue.
minimum(T a, T b) compare a < b, et qu'on l'appelle avec deux objets d'une classe Complexe qui ne surcharge pas l'operator<, que se passe-t-il ?
T fourni, la compilation est rejetée immédiatement.
📝 Exercice d'application pour le TD
Implémentez un conteneur générique File<T> (Queue - FIFO : First In, First Out) sous forme de liste chaînée :
- Structure interne générique
Noeud<T>contenantT m_valeur; Noeud<T>* m_suivant;. - Méthodes
enfiler(const T& val)etT defiler(). - Gestion propre de la mémoire dans le destructeur et le constructeur par recopie.
Suite du cours
Après avoir assimilé les principes de conception des patrons génériques, découvrez la bibliothèque standard C++ (STL) :