Travaux Dirigés 3 : La Généricité et les Templates en C++
Cette séance de travaux dirigés vous permet de mettre en pratique l'ensemble des notions abordées dans le Chapitre 3 : patrons de fonctions, déduction automatique et ambiguïtés de types, patrons de classes avec paramètres non-types (NTTP), spécialisation totale et partielle pour traiter les cas limites (chaînes C-style, booléens), conteneurs génériques dynamiques avec Règle des Trois et implémentation complète d'une file d'attente chaînée (Queue FIFO).
Objectifs pédagogiques de la séance
- Exercice 1 (★☆☆☆☆) : Concevoir des patrons de fonctions (function templates), comprendre la déduction automatique de type par le compilateur, résoudre les ambiguïtés à plusieurs paramètres et implémenter un algorithme générique de recherche séquentielle.
- Exercice 2 (★★☆☆☆) : Manipuler les paramètres non-types de templates (Non-Type Template Parameters - NTTP) avec
TableauStatique<T, N>pour allouer des conteneurs sécurisés directement sur la pile, sans allocation dynamique ni surcoût d'indirection. - Exercice 3 (★★★☆☆) : Maîtriser la spécialisation de patrons (Template Specialization) : résoudre le piège subtil de la comparaison des pointeurs de chaînes littérales
const char*(adresses vs contenu lexicographique) et concevoir un formateur de données adaptatif. - Exercice 4 (★★★★☆) : Développer un conteneur dynamique générique complet
VecteurGenerique<T>combinant templates, gestion de mémoire sur le tas, réallocation géométrique et respect sans faille de la Règle des Trois. - Exercice 5 (★★★★★) : Implémenter de A à Z une structure de données chaînée générique : une File FIFO (Queue) en $O(1)$ avec structure imbriquée
Noeud<T>, gestion rigoureuse des exceptions, duplication profonde de liste et garantie zéro fuite mémoire sous AddressSanitizer.
Consignes de Travail & Outils de Compilation
Commandes de compilation recommandées
Activez la norme C++20 avec détection rigoureuse des avertissements :
g++ -std=c++20 -Wall -Wextra -pedantic exercice.cpp -o exo ./exo
Détection de fuites mémoire & Undefined Behavior
Pour les exercices 4 et 5 impliquant des allocations dynamiques avec templates :
g++ -std=c++20 -fsanitize=address,undefined -g exercice.cpp -o exo ./exo
AddressSanitizer garantit que chaque nœud chaîné et chaque tableau générique alloué sur le tas est proprement désalloué sans double libération.
Patrons de Fonctions & Déduction Automatique de Types
1. Contexte
En C++ classique, l'absence de généricité contraignait le développeur soit à dupliquer les algorithmes pour chaque type de données (int, double, std::string), soit à recourir aux pointeurs génériques non typés void* du C, destructeurs de toute sécurité de typage.
Les patrons de fonctions (Function Templates) permettent de formuler des algorithmes paramétrés par un ou plusieurs types abstraits. C'est le compilateur qui prend en charge la génération automatique (monomorphisation) du code binaire natif adapté à chaque type rencontré.
2. Travail à réaliser
-
Écrivez un patron de fonction
echanger(T& a, T& b)intervertissant les valeurs de deux variables quelconques. Pourquoi les paramètres doivent-ils être transmis par référence non constante ? -
Écrivez un patron de fonction
const T& maximum(const T& a, const T& b)renvoyant une référence constante vers la plus grande des deux valeurs. Expliquez pourquoi le passage parconst T&est ici crucial lorsqueTest un type complexe (commestd::stringou un gros objet). -
Que se passe-t-il si vous tentez d'appeler
maximum(10, 20.5);avec un entier et un flottant ? Pourquoi le compilateur refuse-t-il de compiler ? -
Proposez deux solutions distinctes pour lever cette ambiguïté :
- Solution 1 : Spécifier explicitement le type cible entre chevrons lors de l'appel.
- Solution 2 : Écrire un patron à deux paramètres de types
template <typename T1, typename T2> auto maximumHeterogene(...).
-
Écrivez un algorithme générique
trouverIndice(const T tableau[], std::size_t taille, const T& cible)qui parcourt un tableau brut et renvoie l'indice (int) de la première occurrence égale àcible, ou-1si l'élément est absent. -
Dans le
main(), testez vos patrons avec des entiers, des réels et des objetsstd::string.
Lorsqu'un patron attend deux arguments de même type T (comme maximum(T a, T b)), le compilateur refuse d'effectuer une conversion implicite si deux types différents sont passés (ex: int et double). Il exige que le développeur tranche explicitement en écrivant maximum<double>(10, 20.5) !
Correction détaillée & Analyse de conception
Voici l'implémentation complète des fonctions génériques. Notez la concision du code : une seule fonction echanger ou trouverIndice est écrite, et le compilateur génère les déclinaisons binaires optimales à la volée.
/** * TD 3 - Exercice 1 : Patrons de Fonctions & Algorithmes Fondamentaux * Déduction de type, transmission par const ref, et détection des ambiguïtés en C++20. */ #include <iostream> #include <string> #include <vector> // 1. Patron de fonction pour intervertir deux variables (swap) template <typename T> void echanger(T& a, T& b) { T temporaire = a; a = b; b = temporaire; } // 2. Patron de fonction renvoyant le maximum entre deux valeurs // Transmission par référence constante pour éviter la recopie d'objets lourds template <typename T> const T& maximum(const T& a, const T& b) { return (a < b) ? b : a; } // 3. Patron à deux types distincts pour gérer des arguments hétérogènes // 'auto' en type de retour laisse le compilateur déduire le type commun (C++14/C++20) template <typename T1, typename T2> auto maximumHeterogene(const T1& a, const T2& b) { return (a < b) ? b : a; } // 4. Algorithme générique de recherche séquentielle dans un tableau brut // Renvoie l'indice de la première occurrence trouvée, ou -1 si absent template <typename T> int trouverIndice(const T tableau[], std::size_t taille, const T& cible) { for (std::size_t i = 0; i < taille; ++i) { if (tableau[i] == cible) { return static_cast<int>(i); } } return -1; } // 5. Affichage générique des éléments d'un tableau template <typename T> void afficherTableau(const T tableau[], std::size_t taille, const std::string& nom) { std::cout << nom << " : [ "; for (std::size_t i = 0; i < taille; ++i) { std::cout << tableau[i] << " "; } std::cout << "]\n"; } int main() { std::cout << "=== TD 3 - Exercice 1 : Patrons de Fonctions (Templates) ===\n\n"; // Test 1 : Échange générique std::cout << "--- 1. Test du patron echanger<T>() ---\n"; int x = 42, y = 99; std::cout << "Avant échange entiers : x = " << x << ", y = " << y << "\n"; echanger(x, y); // Déduction automatique : T = int std::cout << "Après échange entiers : x = " << x << ", y = " << y << "\n"; std::string s1 = "Licence", s2 = "Informatique"; std::cout << "Avant échange strings : s1 = \"" << s1 << "\", s2 = \"" << s2 << "\"\n"; echanger(s1, s2); // Déduction automatique : T = std::string std::cout << "Après échange strings : s1 = \"" << s1 << "\", s2 = \"" << s2 << "\"\n"; // Test 2 : Maximum homogène et résolution d'ambiguïté std::cout << "\n--- 2. Test du patron maximum<T>() & Ambiguïtés ---\n"; std::cout << "Max entre 15 et 28 : " << maximum(15, 28) << "\n"; std::cout << "Max entre 3.14 et 2.71 : " << maximum(3.14, 2.71) << "\n"; std::cout << "Max entre 'A' et 'Z' : " << maximum('A', 'Z') << "\n"; // Appel ambigu si types différents : maximum(10, 20.5); // ❌ ERREUR COMPILATION // Levée de l'ambiguïté par spécification explicite du type entre chevrons : std::cout << "Max explicite <double>(10, 20.5) : " << maximum<double>(10, 20.5) << "\n"; // Levée de l'ambiguïté par patron hétérogène : std::cout << "Max hétérogène (10, 20.5) : " << maximumHeterogene(10, 20.5) << "\n"; // Test 3 : Recherche séquentielle générique std::cout << "\n--- 3. Test de l'algorithme trouverIndice<T>() ---\n"; int notes[] = {12, 17, 9, 14, 20, 15}; afficherTableau(notes, 6, "Tableau de notes"); std::cout << "Indice de la note 20 : " << trouverIndice(notes, 6, 20) << "\n"; std::cout << "Indice de la note 18 : " << trouverIndice(notes, 6, 18) << " (absent)\n"; std::string prenoms[] = {"Alice", "Bob", "Claire", "David"}; afficherTableau(prenoms, 4, "Tableau de prénoms"); std::cout << "Indice de \"Claire\" : " << trouverIndice(prenoms, 4, std::string("Claire")) << "\n"; std::cout << "Indice de \"Eve\" : " << trouverIndice(prenoms, 4, std::string("Eve")) << " (absent)\n"; return 0; }
Résultat de l'exécution :
Patrons de Classes & Paramètres Non-Types (NTTP) : TableauStatique<T, N>
1. Contexte
En C++, les templates ne se limitent pas aux paramètres de types : ils acceptent également des paramètres non-types (Non-Type Template Parameters - NTTP), c'est-à-dire des valeurs constantes entières connues dès la compilation.
Cette fonctionnalité permet de concevoir des structures de données de taille fixe allouées intégralement sur la pile (comme std::array en bibliothèque standard). Les avantages sont considérables :
- Zéro allocation dynamique sur le tas : aucun appel à
newnidelete, risque de fuite mémoire nul par construction. - Localité spatiale optimale en mémoire cache : les éléments sont contigus dans le bloc d'activation de pile.
- Typage fort au niveau de la dimension : un tableau de 5 éléments et un tableau de 10 éléments ne sont pas du même type, empêchant tout mélange accidentel.
2. Travail à réaliser
-
Déclarez la classe template
TableauStatique<typename T, std::size_t N>encapsulant un tableau brutT m_elements[N]. Ajoutez une directivestatic_assert(N > 0, "...")pour interdire à la compilation la création d'un tableau de taille nulle. -
Définissez la méthode
constexpr std::size_t taille() const noexceptrenvoyant la constanteN. -
Surchargez l'opérateur d'accès indicé
operator[]en deux versions distinctes :- Une version modifiable :
T& operator[](std::size_t index) noexcept; - Une version en lecture seule :
const T& operator[](std::size_t index) const noexcept;
- Une version modifiable :
-
Écrivez la méthode sécurisée
T& at(std::size_t index)qui lève une exceptionstd::out_of_rangesi l'indice dépasse les bornes $[0, N-1]$. Proposez également la surchargeconst. -
Ajoutez une méthode
void remplir(const T& valeur)qui assigne la valeur fournie à chaque case du conteneur. -
Ajoutez une méthode
T calculerSomme() constcalculant le cumul des éléments (en initialisant un accumulateur avecT total{}). Quelles contraintes cette méthode impose-t-elle sur le typeT? -
Dans le
main(), instanciez unTableauStatique<int, 5>et unTableauStatique<std::string, 3>. Testez la capture d'exception avecat(10). -
Instanciez un
TableauStatique<int, 10> autre;et tentez l'instructiontabEntiers = autre;. Que constatez-vous à la compilation ? Expliquez pourquoi.
En C++, TableauStatique<int, 5> et TableauStatique<int, 10> sont deux types totalement incompatibles pour le compilateur, exactement comme si vous compariez un int et un std::string. Cela interdit toute affectation directe accidentelle entre tableaux de dimensions divergentes.
Correction détaillée & Analyse de conception
Remarquez la taille mémoire affichée par sizeof(*this) : un TableauStatique<int, 5> occupe exactement 20 octets (5 × 4 octets), sans le moindre pointeur supplémentaire ni en-tête dynamique.
/** * TD 3 - Exercice 2 : Patrons de Classes & Paramètres Non-Types (NTTP) * Implémentation d'un conteneur statique sécurisé alloué sur la pile en C++20. */ #include <iostream> #include <string> #include <stdexcept> #include <numeric> // Patron de classe avec un type T et une valeur constante N connue à la compilation template <typename T, std::size_t N> class TableauStatique { // Vérification statique : interdire la création d'un conteneur vide static_assert(N > 0, "La taille du TableauStatique doit être strictement positive !"); private: // Allocation directe sur la pile : zéro indirection, zéro fragmentation du tas T m_elements[N]; public: // Constructeur par défaut : initialise les éléments par défaut TableauStatique() : m_elements{} {} // Taille constante connue à la compilation (constexpr noexcept) constexpr std::size_t taille() const noexcept { return N; } // Accès indicé rapide sans contrôle des bornes (version modifiable) T& operator[](std::size_t index) noexcept { return m_elements[index]; } // Accès indicé rapide (version constante en lecture seule) const T& operator[](std::size_t index) const noexcept { return m_elements[index]; } // Accès indicé sécurisé avec contrôle strict des bornes T& at(std::size_t index) { if (index >= N) { throw std::out_of_range("Index hors limites : " + std::to_string(index) + " >= taille (" + std::to_string(N) + ")"); } return m_elements[index]; } const T& at(std::size_t index) const { if (index >= N) { throw std::out_of_range("Index hors limites : " + std::to_string(index) + " >= taille (" + std::to_string(N) + ")"); } return m_elements[index]; } // Remplit l'intégralité du conteneur avec une valeur donnée void remplir(const T& valeur) { for (std::size_t i = 0; i < N; ++i) { m_elements[i] = valeur; } } // Calcul de la somme : exige que le type T supporte operator+= et l'initialisation T{} T calculerSomme() const { T total{}; for (std::size_t i = 0; i < N; ++i) { total += m_elements[i]; } return total; } // Affichage des éléments void afficher(const std::string& nom) const { std::cout << nom << " [taille=" << N << ", " << sizeof(*this) << " octets sur la pile] : [ "; for (std::size_t i = 0; i < N; ++i) { std::cout << m_elements[i] << " "; } std::cout << "]\n"; } }; int main() { std::cout << "=== TD 3 - Exercice 2 : TableauStatique<T, N> (NTTP) ===\n\n"; // 1. Tableau d'entiers de taille 5 std::cout << "--- 1. Tableau d'entiers sur la pile (TableauStatique<int, 5>) ---\n"; TableauStatique<int, 5> tabEntiers; for (std::size_t i = 0; i < tabEntiers.taille(); ++i) { tabEntiers[i] = static_cast<int>((i + 1) * 10); } tabEntiers.afficher("tabEntiers"); std::cout << "Somme des entiers : " << tabEntiers.calculerSomme() << "\n"; // 2. Tableau de chaînes de caractères de taille 3 std::cout << "\n--- 2. Tableau de chaînes (TableauStatique<std::string, 3>) ---\n"; TableauStatique<std::string, 3> tabMots; tabMots[0] = "Programmation"; tabMots[1] = "Orientée"; tabMots[2] = "Objet"; tabMots.afficher("tabMots"); std::cout << "Concaténation (calculerSomme) : \"" << tabMots.calculerSomme() << "\"\n"; // 3. Démonstration de la méthode sécurisée at() et levée d'exception std::cout << "\n--- 3. Contrôle des bornes avec at() ---\n"; try { std::cout << "Accès valide à l'index 2 : " << tabEntiers.at(2) << "\n"; std::cout << "Tentative d'accès illégal à l'index 10...\n"; tabEntiers.at(10) = 999; // Lève une exception } catch (const std::out_of_range& e) { std::cout << ">> Exception interceptée avec succès : " << e.what() << "\n"; } // 4. Incompatibilité des types instanciés (Typage Statique Fort) TableauStatique<int, 10> autreTab; autreTab.remplir(7); std::cout << "\nautreTab (taille 10) rempli de 7 :\n"; autreTab.afficher("autreTab"); // L'instruction suivante refuserait de compiler : // tabEntiers = autreTab; // ❌ ERREUR COMPILATION : no match for 'operator=' between TableauStatique<int, 5> and TableauStatique<int, 10> std::cout << "\nRemarque : TableauStatique<int, 5> et TableauStatique<int, 10> sont\n" << "deux types distincts et étanches pour le compilateur.\n"; return 0; }
Résultat de l'exécution :
Spécialisation de Templates : Le Piège des Chaînes C (const char*) & Formatage
1. Contexte
Si la programmation générique offre une factorisation élégante, certains types requièrent un traitement algorithmique radicalement spécifique :
- Le piège mortel de
const char*: si l'on compare deux chaînes littérales aveca < b, le compilateur compare leurs adresses mémoire brutes dans le segment de données constantes, et non l'ordre alphabétique du texte ! - L'adaptation d'affichage : formater un booléen sous forme textuelle (
"VRAI"/"FAUX") ou un pointeur sous forme d'adresse hexadécimale nécessite un comportement dédié.
La spécialisation de templates (Template Specialization) permet de fournir une implémentation sur-mesure pour un type donné, tout en conservant une interface générique unifiée pour le reste du programme.
2. Travail à réaliser
-
Considérez le patron générique primaire :
template <typename T> T plusGrand(T a, T b) { return (a < b) ? b : a; }
Expliquez précisément pourquoi l'appelplusGrand("zoo", "arbre")produit un résultat aléatoire ou erroné selon le compilateur et l'agencement mémoire. -
Écrivez la spécialisation totale pour le type
const char*:template <> const char* plusGrand<const char*>(const char* a, const char* b)
Utilisez la fonctionstd::strcmpde la bibliothèque<cstring>pour comparer le contenu lexicographique réel. -
Créez un patron de classe primaire
Formateur<typename T>doté d'une méthode statique :static std::string formater(const T& val)renvoyant la chaîne formatée"[Générique: val]". -
Écrivez la spécialisation totale
template <> class Formateur<bool>qui renvoie"[Booléen: VRAI (TRUE)]"ou"[Booléen: FAUX (FALSE)]". -
Écrivez la spécialisation partielle
template <typename U> class Formateur<U*>capable de formater n'importe quel pointeur en affichant son adresse mémoire et la valeur de la variable pointée (en protégeant le casnullptr). -
Écrivez la spécialisation pour
const char*afin d'afficher la chaîne C entre guillemets sans afficher l'adresse brute du premier caractère. -
Testez l'ensemble des cas dans un
main()et observez comment le compilateur sélectionne automatiquement la version la plus spécialisée.
Lorsqu'un template est instancié, le compilateur recherche en priorité s'il existe une spécialisation totale correspondant exactement au type. Si ce n'est pas le cas, il recherche une spécialisation partielle compatible (ex: U*). Enfin, si aucune spécialisation ne convient, il se rabat sur le patron primaire générique.
Correction détaillée & Analyse de conception
Notez la syntaxe obligatoire template <> sans paramètre entre chevrons pour déclarer une spécialisation totale. L'exécution met en évidence la comparaison alphabétique correcte de "zoo" et "arbre" :
/** * TD 3 - Exercice 3 : Spécialisation de Patrons (Fonctions & Classes) * Résolution du piège des pointeurs C (const char*) et formatage spécialisé en C++20. */ #include <iostream> #include <cstring> #include <string> #include <sstream> #include <iomanip> // ===================================================================== // PARTIE 1 : Patron de fonction et sa spécialisation pour const char* // ===================================================================== // 1. Patron générique primaire template <typename T> T plusGrand(T a, T b) { return (a < b) ? b : a; } // 2. Spécialisation totale (Full Template Specialization) pour const char* // Nécessaire car 'a < b' sur des pointeurs compare les adresses mémoire et non le texte ! template <> const char* plusGrand<const char*>(const char* a, const char* b) { // Comparaison lexicographique via std::strcmp return (std::strcmp(a, b) < 0) ? b : a; } // ===================================================================== // PARTIE 2 : Patron de classe et spécialisations // ===================================================================== // 1. Patron de classe primaire : Formateur générique template <typename T> class Formateur { public: static std::string formater(const T& val) { std::ostringstream oss; oss << "[Générique: " << val << "]"; return oss.str(); } }; // 2. Spécialisation totale pour le type 'bool' template <> class Formateur<bool> { public: static std::string formater(const bool& val) { return val ? "[Booléen: VRAI (TRUE)]" : "[Booléen: FAUX (FALSE)]"; } }; // 3. Spécialisation partielle pour n'importe quel type de pointeur (U*) template <typename U> class Formateur<U*> { public: static std::string formater(U* ptr) { if (ptr == nullptr) { return "[Pointeur: NULLPTR (0x0)]"; } std::ostringstream oss; oss << "[Pointeur @" << static_cast<const void*>(ptr) << " pointant vers: " << *ptr << "]"; return oss.str(); } }; // 4. Spécialisation totale pour const char* (éviter de déférencer comme un simple char) template <> class Formateur<const char*> { public: static std::string formater(const char* texte) { if (texte == nullptr) return "[Chaîne C: NULL]"; return "[Chaîne C: \"" + std::string(texte) + "\"]"; } }; int main() { std::cout << "=== TD 3 - Exercice 3 : Spécialisation de Templates ===\n\n"; // --- 1. Test du patron de fonction plusGrand --- std::cout << "--- 1. Patron de fonction : Problématique de const char* ---\n"; int n1 = 100, n2 = 250; std::cout << "plusGrand(100, 250) : " << plusGrand(n1, n2) << " (entiers standard)\n"; double d1 = 3.1415, d2 = 2.7182; std::cout << "plusGrand(3.1415, 2.7182) : " << plusGrand(d1, d2) << " (réels standard)\n"; // Chaînes C littérales const char* mot1 = "zoo"; const char* mot2 = "arbre"; std::cout << "\nComparaison de deux chaînes C littérales (\"" << mot1 << "\" et \"" << mot2 << "\") :\n"; std::cout << "Adresse de \"" << mot1 << "\" : " << static_cast<const void*>(mot1) << "\n"; std::cout << "Adresse de \"" << mot2 << "\" : " << static_cast<const void*>(mot2) << "\n"; // Grâce à la spécialisation template <>, c'est std::strcmp qui est appelé : const char* gagnant = plusGrand(mot1, mot2); std::cout << "Résultat lexicographique via spécialisation : \"" << gagnant << "\" (correct !)\n"; // --- 2. Test du patron de classe Formateur --- std::cout << "\n--- 2. Patron de classe Formateur<T> et ses spécialisations ---\n"; // Cas générique : entiers, réels, string std::cout << Formateur<int>::formater(42) << "\n"; std::cout << Formateur<double>::formater(19.99) << "\n"; std::cout << Formateur<std::string>::formater(std::string("C++20")) << "\n"; // Cas spécialisé bool std::cout << Formateur<bool>::formater(true) << "\n"; std::cout << Formateur<bool>::formater(false) << "\n"; // Cas spécialisé pour les pointeurs int variable = 777; int* ptrVar = &variable; int* ptrNul = nullptr; std::cout << Formateur<int*>::formater(ptrVar) << "\n"; std::cout << Formateur<int*>::formater(ptrNul) << "\n"; // Cas spécialisé pour const char* std::cout << Formateur<const char*>::formater("Licence Professionnelle Informatique") << "\n"; return 0; }
Résultat de l'exécution :
Conteneur Dynamique Générique & Règle des Trois : VecteurGenerique<T>
1. Contexte
L'objectif de cet exercice est de concevoir un conteneur dynamique universel auto-redimensionnable VecteurGenerique<T> (équivalent pédagogique simplifié de std::vector<T>).
Parce que la classe alloue dynamiquement son tampon de stockage sur le tas via new T[capacite], elle doit impérativement appliquer la Règle des Trois (déjà abordée au TD 1) appliquée cette fois au monde des templates.
2. Travail à réaliser
-
Définissez le patron de classe
VecteurGenerique<typename T>contenant trois attributs privés : un pointeurT* m_donnees, une taille actuellestd::size_t m_tailleet une capacité allouéestd::size_t m_capacite. -
Écrivez le constructeur paramétré
explicit VecteurGenerique(std::size_t capaciteInitiale = 2)qui alloue le tableau dynamique sur le tas. -
Règle des Trois (Pilier 1 - Destructeur) : Écrivez
~VecteurGenerique()libérant la ressource avecdelete[] m_donnees. -
Règle des Trois (Pilier 2 - Constructeur de recopie) : Écrivez
VecteurGenerique(const VecteurGenerique<T>& source)réalisant une copie profonde de tous les éléments $T$. -
Règle des Trois (Pilier 3 - Opérateur d'affectation) : Écrivez
VecteurGenerique<T>& operator=(const VecteurGenerique<T>& source)avec vérification rigoureuse de l'auto-affectation (this == &source) et réallocation sécurisée. -
Implémentez la méthode de croissance
void reserver(std::size_t nouvelleCapacite): alloue un nouveau tampon, copie les éléments existants, libère l'ancien tampon et met à jour les pointeurs. -
Implémentez
void push_back(const T& valeur): si la capacité maximale est atteinte, doublez automatiquement la capacité avant d'insérer l'élément. -
Ajoutez
pop_back(),operator[],at(),getTaille(),getCapacite()etafficher(). -
Dans le
main(), testez votre conteneur avec desint, puis avec desstd::string. Vérifiez l'indépendance de mémoire lors d'une copie et testez l'auto-affectationv = v;.
Si l'on agrandissait le conteneur de seulement $+1$ case à chaque push_back(), chaque ajout obligerait à copier l'intégralité du tableau en mémoire, conduisant à une complexité algorithmique catastrophique en $O(N^2)$. En doublant la capacité à chaque saturation, la complexité amortie par insertion devient optimale : $O(1)$ amorti !
Correction détaillée & Analyse architecturale
Ce code démontre la puissance conjointe des templates et de l'idiome RAII : la gestion mémoire s'adapte sans effort aux types primitifs (int) comme aux types gérant eux-mêmes des ressources (std::string).
/** * TD 3 - Exercice 4 : Conteneur Dynamique Générique & Règle des Trois * Implémentation complète d'un VecteurGenerique<T> auto-redimensionnable en C++20. */ #include <iostream> #include <string> #include <stdexcept> #include <algorithm> template <typename T> class VecteurGenerique { private: T* m_donnees; std::size_t m_taille; std::size_t m_capacite; public: // 1. Constructeur par défaut (allocation minimale) explicit VecteurGenerique(std::size_t capaciteInitiale = 2) : m_donnees(capaciteInitiale > 0 ? new T[capaciteInitiale] : nullptr), m_taille(0), m_capacite(capaciteInitiale) { std::cout << " [+] VecteurGenerique(@" << this << ") alloué (capacité: " << m_capacite << ")\n"; } // 2. Destructeur (Pilier 1 de la Règle des Trois) ~VecteurGenerique() { std::cout << " [-] ~VecteurGenerique(@" << this << ") libération mémoire tas @" << static_cast<void*>(m_donnees) << "\n"; delete[] m_donnees; } // 3. Constructeur par recopie profonde (Pilier 2 de la Règle des Trois) VecteurGenerique(const VecteurGenerique<T>& source) : m_donnees(source.m_capacite > 0 ? new T[source.m_capacite] : nullptr), m_taille(source.m_taille), m_capacite(source.m_capacite) { for (std::size_t i = 0; i < m_taille; ++i) { m_donnees[i] = source.m_donnees[i]; // Recopie de chaque élément T } std::cout << " [C] VecteurGenerique COPIÉ depuis @" << &source << " vers @" << this << " (taille: " << m_taille << ")\n"; } // 4. Opérateur d'affectation sécurisé (Pilier 3 de la Règle des Trois) VecteurGenerique<T>& operator=(const VecteurGenerique<T>& source) { // Protection vitale contre l'auto-affectation (v = v) if (this == &source) { std::cout << " [=] Auto-affectation détectée sur @" << this << ", ignorée.\n"; return *this; } // Allocation préalable du nouveau tampon (sécurité aux exceptions) T* nouveauTampon = source.m_capacite > 0 ? new T[source.m_capacite] : nullptr; for (std::size_t i = 0; i < source.m_taille; ++i) { nouveauTampon[i] = source.m_donnees[i]; } // Libération de l'ancienne mémoire delete[] m_donnees; // Mise à jour des métadonnées m_donnees = nouveauTampon; m_taille = source.m_taille; m_capacite = source.m_capacite; std::cout << " [=] VecteurGenerique AFFECTÉ depuis @" << &source << " vers @" << this << "\n"; return *this; } // Redimensionnement automatique (stratégie géométrique par doublement) void reserver(std::size_t nouvelleCapacite) { if (nouvelleCapacite <= m_capacite) return; T* nouveauTampon = new T[nouvelleCapacite]; for (std::size_t i = 0; i < m_taille; ++i) { nouveauTampon[i] = m_donnees[i]; } delete[] m_donnees; m_donnees = nouveauTampon; m_capacite = nouvelleCapacite; std::cout << " -> Réallocation dynamique (@" << this << ") : capacité portée à " << m_capacite << "\n"; } // Ajout d'un élément en fin de conteneur void push_back(const T& valeur) { if (m_taille == m_capacite) { reserver(m_capacite == 0 ? 2 : m_capacite * 2); } m_donnees[m_taille++] = valeur; } // Retrait du dernier élément void pop_back() { if (m_taille == 0) { throw std::underflow_error("Impossible de faire pop_back() sur un vecteur vide !"); } --m_taille; } // Accesseurs std::size_t getTaille() const noexcept { return m_taille; } std::size_t getCapacite() const noexcept { return m_capacite; } bool estVide() const noexcept { return m_taille == 0; } // Accès aux éléments T& operator[](std::size_t index) noexcept { return m_donnees[index]; } const T& operator[](std::size_t index) const noexcept { return m_donnees[index]; } T& at(std::size_t index) { if (index >= m_taille) { throw std::out_of_range("Index hors limites (" + std::to_string(index) + " >= " + std::to_string(m_taille) + ")"); } return m_donnees[index]; } const T& at(std::size_t index) const { if (index >= m_taille) { throw std::out_of_range("Index hors limites (" + std::to_string(index) + " >= " + std::to_string(m_taille) + ")"); } return m_donnees[index]; } void afficher(const std::string& nom) const { std::cout << nom << " [taille=" << m_taille << ", cap=" << m_capacite << "] : [ "; for (std::size_t i = 0; i < m_taille; ++i) { std::cout << m_donnees[i] << " "; } std::cout << "]\n"; } }; int main() { std::cout << "=== TD 3 - Exercice 4 : VecteurGenerique<T> & Règle des Trois ===\n\n"; std::cout << "--- 1. Utilisation avec des entiers et redimensionnement dynamique ---\n"; { VecteurGenerique<int> v(2); v.push_back(10); v.push_back(20); v.afficher("v après 2 ajouts"); std::cout << "Ajout d'un 3ème élément (déclenche réallocation) :\n"; v.push_back(30); v.afficher("v après 3ème ajout"); v.push_back(40); v.push_back(50); v.afficher("v après 5 ajouts"); std::cout << "\n--- 2. Test du Constructeur de Recopie (copie profonde) ---\n"; VecteurGenerique<int> copie = v; copie.afficher("copie"); std::cout << "Modification de copie[0] = 999...\n"; copie[0] = 999; std::cout << "copie[0] = " << copie[0] << " | original v[0] = " << v[0] << " (indépendance mémoire vérifiée !)\n"; std::cout << "\n--- 3. Test de l'Affectation et de l'Auto-affectation ---\n"; VecteurGenerique<int> autre(1); autre = v; autre.afficher("autre après affectation"); std::cout << "Test critique autre = autre :\n"; autre = autre; autre.afficher("autre après auto-affectation"); std::cout << "\nSortie de bloc (destructions automatiques RAII) :\n"; } std::cout << "\n--- 4. Utilisation avec des chaînes de caractères (std::string) ---\n"; { VecteurGenerique<std::string> mots; mots.push_back("C++"); mots.push_back("Templates"); mots.push_back("Généricité"); mots.push_back("RAII"); mots.afficher("mots"); } return 0; }
Résultat de l'exécution :
Structure Chaînée Générique : File d'Attente FIFO (Queue<T>)
1. Contexte
Cet exercice concrétise l'exercice d'application proposé à la fin du Chapitre 3 : la conception complète d'une file d'attente générique (Queue FIFO - First In, First Out) sous forme de liste simplement chaînée.
Contrairement à un tableau dynamique qui réalloue des blocs contigus, une liste chaînée alloue individuellement chaque élément sous forme de maillon (nœud) relié au suivant par un pointeur. Pour garantir des performances optimales en temps constant $O(1)$, la file maintient deux pointeurs stratégiques :
m_tete(Front) : pointeur sur le premier élément entré, qui sera le premier extrait pardefiler().m_queue(Back) : pointeur sur le dernier maillon inséré parenfiler().
2. Travail à réaliser
-
Créez le patron de classe
FileGenerique<typename T>. Définissez en section privée la structure imbriquéestruct Noeud { T donnee; Noeud* suivant; ... };. -
Déclarez les attributs privés :
Noeud* m_tete;,Noeud* m_queue;etstd::size_t m_taille;. -
Écrivez le constructeur par défaut initialisant une file vide (pointeurs à
nullptret taille à 0). -
Écrivez la méthode
void vider()qui parcourt la liste et libère individuellement chaque nœud avecdelete. Utilisez cette méthode dans le destructeur~FileGenerique(). -
Implémentez
void enfiler(const T& valeur): alloue un nouveau nœud, l'ajoute derrièrem_queueet met à jour les pointeurs. Vérifiez que l'opération s'effectue en temps constant $O(1)$. -
Implémentez
T defiler(): extrait la valeur en tête, avancem_tete, libère le nœud extrait avecdeleteet renvoie la valeur. Si la file est vide, levez une exceptionstd::underflow_error. -
Ajoutez les méthodes
const T& premier() const,bool estVide() const noexceptetstd::size_t getTaille() const noexcept. -
Règle des Trois pour structure chaînée :
- Écrivez le constructeur de recopie : il doit parcourir la file source de la tête vers la queue et appeler
enfiler()pour reconstruire une copie distincte dans le même ordre FIFO. - Écrivez l'opérateur d'affectation avec protection contre l'auto-affectation, nettoyage préalable par
vider()et duplication des maillons.
- Écrivez le constructeur de recopie : il doit parcourir la file source de la tête vers la queue et appeler
-
Dans le
main(), simulez un système de gestion de spooler d'impression (FileGenerique<std::string>). Vérifiez l'indépendance de la copie et la bonne interception de l'exception lors d'un défilement sur file vide.
Correction complète & Analyse algorithmique
Voici la solution complète de la file générique FIFO. Notez l'élégance de la boucle de recopie dans le constructeur de copie, qui réutilise directement enfiler() pour garantir un ordonnancement fidèle :
/** * TD 3 - Exercice 5 : Structure de Données Chaînée Générique * Conception intégrale d'une File<T> (Queue FIFO) en C++20 avec Règle des Trois. */ #include <iostream> #include <string> #include <stdexcept> template <typename T> class FileGenerique { private: // Structure interne représentant un maillon de la liste chaînée struct Noeud { T donnee; Noeud* suivant; Noeud(const T& val) : donnee(val), suivant(nullptr) {} }; Noeud* m_tete; // Pointeur vers le premier élément à sortir (Tête / Front) Noeud* m_queue; // Pointeur vers le dernier élément entré (Queue / Back) std::size_t m_taille; // Nombre d'éléments présents dans la file public: // 1. Constructeur par défaut (file initialement vide) FileGenerique() : m_tete(nullptr), m_queue(nullptr), m_taille(0) { std::cout << " [+] FileGenerique(@" << this << ") initialisée vide\n"; } // 2. Destructeur (libération de tous les nœuds chaînés) ~FileGenerique() { std::cout << " [-] ~FileGenerique(@" << this << ") destruction de " << m_taille << " nœud(s)...\n"; vider(); } // 3. Constructeur par recopie (copie profonde de la liste chaînée) FileGenerique(const FileGenerique<T>& source) : m_tete(nullptr), m_queue(nullptr), m_taille(0) { Noeud* courant = source.m_tete; while (courant != nullptr) { enfiler(courant->donnee); // Reconstruit fidèlement les nœuds dans l'ordre FIFO courant = courant->suivant; } std::cout << " [C] FileGenerique COPIÉE depuis @" << &source << " vers @" << this << " (" << m_taille << " éléments)\n"; } // 4. Opérateur d'affectation sécurisé FileGenerique<T>& operator=(const FileGenerique<T>& source) { if (this == &source) { std::cout << " [=] Auto-affectation détectée sur @" << this << ", ignorée.\n"; return *this; } // Nettoyer les éléments actuels vider(); // Dupliquer les nœuds de la source Noeud* courant = source.m_tete; while (courant != nullptr) { enfiler(courant->donnee); courant = courant->suivant; } std::cout << " [=] FileGenerique AFFECTÉE depuis @" << &source << " vers @" << this << "\n"; return *this; } // Vide complètement la file et désalloue les nœuds void vider() { while (m_tete != nullptr) { Noeud* aSupprimer = m_tete; m_tete = m_tete->suivant; delete aSupprimer; } m_queue = nullptr; m_taille = 0; } // Enfile un élément en queue de file (Complexité O(1)) void enfiler(const T& valeur) { Noeud* nouveau = new Noeud(valeur); if (estVide()) { m_tete = nouveau; m_queue = nouveau; } else { m_queue->suivant = nouveau; m_queue = nouveau; } ++m_taille; } // Défile et renvoie le premier élément en tête de file (Complexité O(1)) T defiler() { if (estVide()) { throw std::underflow_error("Erreur : Impossible de défiler une file vide !"); } Noeud* ancien = m_tete; T valeurSortie = ancien->donnee; m_tete = m_tete->suivant; if (m_tete == nullptr) { m_queue = nullptr; // La file est devenue complètement vide } delete ancien; --m_taille; return valeurSortie; } // Consultation du premier élément sans le retirer const T& premier() const { if (estVide()) { throw std::underflow_error("File vide : aucun élément en tête !"); } return m_tete->donnee; } bool estVide() const noexcept { return m_tete == nullptr; } std::size_t getTaille() const noexcept { return m_taille; } // Affichage de la file du premier sorti vers le dernier entré void afficher(const std::string& nom) const { std::cout << nom << " [taille=" << m_taille << "] (Tête -> Queue) : [ "; Noeud* courant = m_tete; while (courant != nullptr) { std::cout << courant->donnee; if (courant->suivant != nullptr) std::cout << " -> "; courant = courant->suivant; } std::cout << " ]\n"; } }; int main() { std::cout << "=== TD 3 - Exercice 5 : FileGenerique<T> (Queue FIFO Chaînée) ===\n\n"; std::cout << "--- 1. File de tâches d'impression (std::string) ---\n"; { FileGenerique<std::string> spooler; spooler.enfiler("Doc_Compta.pdf"); spooler.enfiler("Rapport_Stage.docx"); spooler.enfiler("Photo_Identite.png"); spooler.afficher("Spooler d'impression"); std::cout << "Prochain document à imprimer : \"" << spooler.premier() << "\"\n\n"; std::cout << "Traitement (défilement FIFO) :\n"; std::cout << "-> Impression en cours : " << spooler.defiler() << "\n"; std::cout << "-> Impression en cours : " << spooler.defiler() << "\n"; spooler.afficher("Spooler restant"); std::cout << "\nAjout d'une nouvelle tâche urgente :\n"; spooler.enfiler("Facture_Client.pdf"); spooler.afficher("Spooler mis à jour"); std::cout << "\n--- 2. Test du Constructeur de Recopie (indépendance des listes) ---\n"; FileGenerique<std::string> copieSpooler = spooler; copieSpooler.afficher("Copie du spooler"); std::cout << "Défilement sur la copie : " << copieSpooler.defiler() << "\n"; copieSpooler.afficher("Copie après défilement"); spooler.afficher("Original (intact !)"); std::cout << "\n--- 3. Affectation et Auto-affectation ---\n"; FileGenerique<std::string> autre; autre = spooler; autre.afficher("Autre après affectation"); autre = autre; // Test auto-affectation autre.afficher("Autre après auto-affectation"); std::cout << "\nSortie de bloc : libération mémoire nœud par nœud...\n"; } std::cout << "\n--- 4. File d'entiers & Gestion des Exceptions ---\n"; { FileGenerique<int> fileEntiers; fileEntiers.enfiler(100); fileEntiers.enfiler(200); std::cout << "Élément retiré : " << fileEntiers.defiler() << "\n"; std::cout << "Élément retiré : " << fileEntiers.defiler() << "\n"; try { std::cout << "Tentative de défiler une file vide...\n"; fileEntiers.defiler(); // Lève underflow_error } catch (const std::underflow_error& e) { std::cout << ">> Exception interceptée avec succès : " << e.what() << "\n"; } } return 0; }
Résultat de l'exécution :
🎉 Félicitations : Vous maîtrisez la Généricité et les Templates C++ !
Au terme de ce TD 3, vous avez acquis l'une des compétences les plus puissantes du développeur C++ moderne :
- Vous savez écrire des algorithmes génériques indépendants du type tout en préservant un typage statique strict et des performances optimales.
- Vous maîtrisez les paramètres non-types (NTTP) pour créer des conteneurs statiques alloués sur la pile sans surcoût dynamique.
- Vous savez spécialiser des patrons pour gérer les cas limites (comme les chaînes
const char*ou les pointeurs). - Vous combinez la généricité avec la gestion saine des ressources (Règle des Trois et RAII) sur des structures dynamiques et chaînées.