Niveau : Licence 3 Fiche TD n°3 Volume : 2h00 Pratique C++20

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.

Exercice 1

Patrons de Fonctions & Déduction Automatique de Types

★☆☆☆☆ Difficulté : Débutant ⏱️ 20 min Concepts : template <typename T>, Déduction, const T&, Ambiguïté

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

  1. É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 ?
  2. É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 par const T& est ici crucial lorsque T est un type complexe (comme std::string ou un gros objet).
  3. 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 ?
  4. 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(...).
  5. É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 -1 si l'élément est absent.
  6. Dans le main(), testez vos patrons avec des entiers, des réels et des objets std::string.
💡 Indice pédagogique : Déduction stricte en C++

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.

CPPtd3_ex1_fonctions_generiques.cpp
  1. /**
  2.  * TD 3 - Exercice 1 : Patrons de Fonctions & Algorithmes Fondamentaux
  3.  * Déduction de type, transmission par const ref, et détection des ambiguïtés en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <vector>
  9.  
  10. // 1. Patron de fonction pour intervertir deux variables (swap)
  11. template <typename T>
  12. void echanger(T& a, T& b) {
  13. T temporaire = a;
  14. a = b;
  15. b = temporaire;
  16. }
  17.  
  18. // 2. Patron de fonction renvoyant le maximum entre deux valeurs
  19. // Transmission par référence constante pour éviter la recopie d'objets lourds
  20. template <typename T>
  21. const T& maximum(const T& a, const T& b) {
  22. return (a < b) ? b : a;
  23. }
  24.  
  25. // 3. Patron à deux types distincts pour gérer des arguments hétérogènes
  26. // 'auto' en type de retour laisse le compilateur déduire le type commun (C++14/C++20)
  27. template <typename T1, typename T2>
  28. auto maximumHeterogene(const T1& a, const T2& b) {
  29. return (a < b) ? b : a;
  30. }
  31.  
  32. // 4. Algorithme générique de recherche séquentielle dans un tableau brut
  33. // Renvoie l'indice de la première occurrence trouvée, ou -1 si absent
  34. template <typename T>
  35. int trouverIndice(const T tableau[], std::size_t taille, const T& cible) {
  36. for (std::size_t i = 0; i < taille; ++i) {
  37. if (tableau[i] == cible) {
  38. return static_cast<int>(i);
  39. }
  40. }
  41. return -1;
  42. }
  43.  
  44. // 5. Affichage générique des éléments d'un tableau
  45. template <typename T>
  46. void afficherTableau(const T tableau[], std::size_t taille, const std::string& nom) {
  47. std::cout << nom << " : [ ";
  48. for (std::size_t i = 0; i < taille; ++i) {
  49. std::cout << tableau[i] << " ";
  50. }
  51. std::cout << "]\n";
  52. }
  53.  
  54. int main() {
  55. std::cout << "=== TD 3 - Exercice 1 : Patrons de Fonctions (Templates) ===\n\n";
  56.  
  57. // Test 1 : Échange générique
  58. std::cout << "--- 1. Test du patron echanger<T>() ---\n";
  59. int x = 42, y = 99;
  60. std::cout << "Avant échange entiers : x = " << x << ", y = " << y << "\n";
  61. echanger(x, y); // Déduction automatique : T = int
  62. std::cout << "Après échange entiers : x = " << x << ", y = " << y << "\n";
  63.  
  64. std::string s1 = "Licence", s2 = "Informatique";
  65. std::cout << "Avant échange strings : s1 = \"" << s1 << "\", s2 = \"" << s2 << "\"\n";
  66. echanger(s1, s2); // Déduction automatique : T = std::string
  67. std::cout << "Après échange strings : s1 = \"" << s1 << "\", s2 = \"" << s2 << "\"\n";
  68.  
  69. // Test 2 : Maximum homogène et résolution d'ambiguïté
  70. std::cout << "\n--- 2. Test du patron maximum<T>() & Ambiguïtés ---\n";
  71. std::cout << "Max entre 15 et 28 : " << maximum(15, 28) << "\n";
  72. std::cout << "Max entre 3.14 et 2.71 : " << maximum(3.14, 2.71) << "\n";
  73. std::cout << "Max entre 'A' et 'Z' : " << maximum('A', 'Z') << "\n";
  74.  
  75. // Appel ambigu si types différents : maximum(10, 20.5); // ❌ ERREUR COMPILATION
  76. // Levée de l'ambiguïté par spécification explicite du type entre chevrons :
  77. std::cout << "Max explicite <double>(10, 20.5) : " << maximum<double>(10, 20.5) << "\n";
  78.  
  79. // Levée de l'ambiguïté par patron hétérogène :
  80. std::cout << "Max hétérogène (10, 20.5) : " << maximumHeterogene(10, 20.5) << "\n";
  81.  
  82. // Test 3 : Recherche séquentielle générique
  83. std::cout << "\n--- 3. Test de l'algorithme trouverIndice<T>() ---\n";
  84. int notes[] = {12, 17, 9, 14, 20, 15};
  85. afficherTableau(notes, 6, "Tableau de notes");
  86. std::cout << "Indice de la note 20 : " << trouverIndice(notes, 6, 20) << "\n";
  87. std::cout << "Indice de la note 18 : " << trouverIndice(notes, 6, 18) << " (absent)\n";
  88.  
  89. std::string prenoms[] = {"Alice", "Bob", "Claire", "David"};
  90. afficherTableau(prenoms, 4, "Tableau de prénoms");
  91. std::cout << "Indice de \"Claire\" : " << trouverIndice(prenoms, 4, std::string("Claire")) << "\n";
  92. std::cout << "Indice de \"Eve\" : " << trouverIndice(prenoms, 4, std::string("Eve")) << " (absent)\n";
  93.  
  94. return 0;
  95. }

Résultat de l'exécution :

=== TD 3 - Exercice 1 : Patrons de Fonctions (Templates) === --- 1. Test du patron echanger<T>() --- Avant échange entiers : x = 42, y = 99 Après échange entiers : x = 99, y = 42 Avant échange strings : s1 = "Licence", s2 = "Informatique" Après échange strings : s1 = "Informatique", s2 = "Licence" --- 2. Test du patron maximum<T>() & Ambiguïtés --- Max entre 15 et 28 : 28 Max entre 3.14 et 2.71 : 3.14 Max entre 'A' et 'Z' : Z Max explicite <double>(10, 20.5) : 20.5 Max hétérogène (10, 20.5) : 20.5 --- 3. Test de l'algorithme trouverIndice<T>() --- Tableau de notes : [ 12 17 9 14 20 15 ] Indice de la note 20 : 4 Indice de la note 18 : -1 (absent) Tableau de prénoms : [ Alice Bob Claire David ] Indice de "Claire" : 2 Indice de "Eve" : -1 (absent)
Exercice 2

Patrons de Classes & Paramètres Non-Types (NTTP) : TableauStatique<T, N>

★★☆☆☆ Difficulté : Facile à Intermédiaire ⏱️ 25 min Concepts : template <typename T, size_t N>, NTTP, operator[], at(), Typage Fort

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 à new ni delete, 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

  1. Déclarez la classe template TableauStatique<typename T, std::size_t N> encapsulant un tableau brut T m_elements[N]. Ajoutez une directive static_assert(N > 0, "...") pour interdire à la compilation la création d'un tableau de taille nulle.
  2. Définissez la méthode constexpr std::size_t taille() const noexcept renvoyant la constante N.
  3. 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;
  4. Écrivez la méthode sécurisée T& at(std::size_t index) qui lève une exception std::out_of_range si l'indice dépasse les bornes $[0, N-1]$. Proposez également la surcharge const.
  5. Ajoutez une méthode void remplir(const T& valeur) qui assigne la valeur fournie à chaque case du conteneur.
  6. Ajoutez une méthode T calculerSomme() const calculant le cumul des éléments (en initialisant un accumulateur avec T total{}). Quelles contraintes cette méthode impose-t-elle sur le type T ?
  7. Dans le main(), instanciez un TableauStatique<int, 5> et un TableauStatique<std::string, 3>. Testez la capture d'exception avec at(10).
  8. Instanciez un TableauStatique<int, 10> autre; et tentez l'instruction tabEntiers = autre;. Que constatez-vous à la compilation ? Expliquez pourquoi.
⚠️ Typage Fort : La dimension fait partie intégrante du type !

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.

CPPtd3_ex2_tableau_statique.cpp
  1. /**
  2.  * TD 3 - Exercice 2 : Patrons de Classes & Paramètres Non-Types (NTTP)
  3.  * Implémentation d'un conteneur statique sécurisé alloué sur la pile en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <stdexcept>
  9. #include <numeric>
  10.  
  11. // Patron de classe avec un type T et une valeur constante N connue à la compilation
  12. template <typename T, std::size_t N>
  13. class TableauStatique {
  14. // Vérification statique : interdire la création d'un conteneur vide
  15. static_assert(N > 0, "La taille du TableauStatique doit être strictement positive !");
  16.  
  17. private:
  18. // Allocation directe sur la pile : zéro indirection, zéro fragmentation du tas
  19. T m_elements[N];
  20.  
  21. public:
  22. // Constructeur par défaut : initialise les éléments par défaut
  23. TableauStatique() : m_elements{} {}
  24.  
  25. // Taille constante connue à la compilation (constexpr noexcept)
  26. constexpr std::size_t taille() const noexcept {
  27. return N;
  28. }
  29.  
  30. // Accès indicé rapide sans contrôle des bornes (version modifiable)
  31. T& operator[](std::size_t index) noexcept {
  32. return m_elements[index];
  33. }
  34.  
  35. // Accès indicé rapide (version constante en lecture seule)
  36. const T& operator[](std::size_t index) const noexcept {
  37. return m_elements[index];
  38. }
  39.  
  40. // Accès indicé sécurisé avec contrôle strict des bornes
  41. T& at(std::size_t index) {
  42. if (index >= N) {
  43. throw std::out_of_range("Index hors limites : " + std::to_string(index)
  44. + " >= taille (" + std::to_string(N) + ")");
  45. }
  46. return m_elements[index];
  47. }
  48.  
  49. const T& at(std::size_t index) const {
  50. if (index >= N) {
  51. throw std::out_of_range("Index hors limites : " + std::to_string(index)
  52. + " >= taille (" + std::to_string(N) + ")");
  53. }
  54. return m_elements[index];
  55. }
  56.  
  57. // Remplit l'intégralité du conteneur avec une valeur donnée
  58. void remplir(const T& valeur) {
  59. for (std::size_t i = 0; i < N; ++i) {
  60. m_elements[i] = valeur;
  61. }
  62. }
  63.  
  64. // Calcul de la somme : exige que le type T supporte operator+= et l'initialisation T{}
  65. T calculerSomme() const {
  66. T total{};
  67. for (std::size_t i = 0; i < N; ++i) {
  68. total += m_elements[i];
  69. }
  70. return total;
  71. }
  72.  
  73. // Affichage des éléments
  74. void afficher(const std::string& nom) const {
  75. std::cout << nom << " [taille=" << N << ", " << sizeof(*this) << " octets sur la pile] : [ ";
  76. for (std::size_t i = 0; i < N; ++i) {
  77. std::cout << m_elements[i] << " ";
  78. }
  79. std::cout << "]\n";
  80. }
  81. };
  82.  
  83. int main() {
  84. std::cout << "=== TD 3 - Exercice 2 : TableauStatique<T, N> (NTTP) ===\n\n";
  85.  
  86. // 1. Tableau d'entiers de taille 5
  87. std::cout << "--- 1. Tableau d'entiers sur la pile (TableauStatique<int, 5>) ---\n";
  88. TableauStatique<int, 5> tabEntiers;
  89. for (std::size_t i = 0; i < tabEntiers.taille(); ++i) {
  90. tabEntiers[i] = static_cast<int>((i + 1) * 10);
  91. }
  92. tabEntiers.afficher("tabEntiers");
  93. std::cout << "Somme des entiers : " << tabEntiers.calculerSomme() << "\n";
  94.  
  95. // 2. Tableau de chaînes de caractères de taille 3
  96. std::cout << "\n--- 2. Tableau de chaînes (TableauStatique<std::string, 3>) ---\n";
  97. TableauStatique<std::string, 3> tabMots;
  98. tabMots[0] = "Programmation";
  99. tabMots[1] = "Orientée";
  100. tabMots[2] = "Objet";
  101. tabMots.afficher("tabMots");
  102. std::cout << "Concaténation (calculerSomme) : \"" << tabMots.calculerSomme() << "\"\n";
  103.  
  104. // 3. Démonstration de la méthode sécurisée at() et levée d'exception
  105. std::cout << "\n--- 3. Contrôle des bornes avec at() ---\n";
  106. try {
  107. std::cout << "Accès valide à l'index 2 : " << tabEntiers.at(2) << "\n";
  108. std::cout << "Tentative d'accès illégal à l'index 10...\n";
  109. tabEntiers.at(10) = 999; // Lève une exception
  110. } catch (const std::out_of_range& e) {
  111. std::cout << ">> Exception interceptée avec succès : " << e.what() << "\n";
  112. }
  113.  
  114. // 4. Incompatibilité des types instanciés (Typage Statique Fort)
  115. TableauStatique<int, 10> autreTab;
  116. autreTab.remplir(7);
  117. std::cout << "\nautreTab (taille 10) rempli de 7 :\n";
  118. autreTab.afficher("autreTab");
  119.  
  120. // L'instruction suivante refuserait de compiler :
  121. // tabEntiers = autreTab;
  122. // ❌ ERREUR COMPILATION : no match for 'operator=' between TableauStatique<int, 5> and TableauStatique<int, 10>
  123. std::cout << "\nRemarque : TableauStatique<int, 5> et TableauStatique<int, 10> sont\n"
  124. << "deux types distincts et étanches pour le compilateur.\n";
  125.  
  126. return 0;
  127. }

Résultat de l'exécution :

=== TD 3 - Exercice 2 : TableauStatique<T, N> (NTTP) === --- 1. Tableau d'entiers sur la pile (TableauStatique<int, 5>) --- tabEntiers [taille=5, 20 octets sur la pile] : [ 10 20 30 40 50 ] Somme des entiers : 150 --- 2. Tableau de chaînes (TableauStatique<std::string, 3>) --- tabMots [taille=3, 96 octets sur la pile] : [ Programmation Orientée Objet ] Concaténation (calculerSomme) : "ProgrammationOrientéeObjet" --- 3. Contrôle des bornes avec at() --- Accès valide à l'index 2 : 30 Tentative d'accès illégal à l'index 10... >> Exception interceptée avec succès : Index hors limites : 10 >= taille (5) autreTab (taille 10) rempli de 7 : autreTab [taille=10, 40 octets sur la pile] : [ 7 7 7 7 7 7 7 7 7 7 ] Remarque : TableauStatique<int, 5> et TableauStatique<int, 10> sont deux types distincts et étanches pour le compilateur.
Exercice 3

Spécialisation de Templates : Le Piège des Chaînes C (const char*) & Formatage

★★★☆☆ Difficulté : Intermédiaire ⏱️ 25 min Concepts : template <>, Spécialisation totale/partielle, const char*, std::strcmp

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 avec a < 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

  1. 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'appel plusGrand("zoo", "arbre") produit un résultat aléatoire ou erroné selon le compilateur et l'agencement mémoire.
  2. Écrivez la spécialisation totale pour le type const char* :
    template <> const char* plusGrand<const char*>(const char* a, const char* b)
    Utilisez la fonction std::strcmp de la bibliothèque <cstring> pour comparer le contenu lexicographique réel.
  3. 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]".
  4. Écrivez la spécialisation totale template <> class Formateur<bool> qui renvoie "[Booléen: VRAI (TRUE)]" ou "[Booléen: FAUX (FALSE)]".
  5. É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 cas nullptr).
  6. É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.
  7. Testez l'ensemble des cas dans un main() et observez comment le compilateur sélectionne automatiquement la version la plus spécialisée.
🚨 Règle de sélection : Du plus spécifique au plus général

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" :

CPPtd3_ex3_specialisation.cpp
  1. /**
  2.  * TD 3 - Exercice 3 : Spécialisation de Patrons (Fonctions & Classes)
  3.  * Résolution du piège des pointeurs C (const char*) et formatage spécialisé en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <cstring>
  8. #include <string>
  9. #include <sstream>
  10. #include <iomanip>
  11.  
  12. // =====================================================================
  13. // PARTIE 1 : Patron de fonction et sa spécialisation pour const char*
  14. // =====================================================================
  15.  
  16. // 1. Patron générique primaire
  17. template <typename T>
  18. T plusGrand(T a, T b) {
  19. return (a < b) ? b : a;
  20. }
  21.  
  22. // 2. Spécialisation totale (Full Template Specialization) pour const char*
  23. // Nécessaire car 'a < b' sur des pointeurs compare les adresses mémoire et non le texte !
  24. template <>
  25. const char* plusGrand<const char*>(const char* a, const char* b) {
  26. // Comparaison lexicographique via std::strcmp
  27. return (std::strcmp(a, b) < 0) ? b : a;
  28. }
  29.  
  30. // =====================================================================
  31. // PARTIE 2 : Patron de classe et spécialisations
  32. // =====================================================================
  33.  
  34. // 1. Patron de classe primaire : Formateur générique
  35. template <typename T>
  36. class Formateur {
  37. public:
  38. static std::string formater(const T& val) {
  39. std::ostringstream oss;
  40. oss << "[Générique: " << val << "]";
  41. return oss.str();
  42. }
  43. };
  44.  
  45. // 2. Spécialisation totale pour le type 'bool'
  46. template <>
  47. class Formateur<bool> {
  48. public:
  49. static std::string formater(const bool& val) {
  50. return val ? "[Booléen: VRAI (TRUE)]" : "[Booléen: FAUX (FALSE)]";
  51. }
  52. };
  53.  
  54. // 3. Spécialisation partielle pour n'importe quel type de pointeur (U*)
  55. template <typename U>
  56. class Formateur<U*> {
  57. public:
  58. static std::string formater(U* ptr) {
  59. if (ptr == nullptr) {
  60. return "[Pointeur: NULLPTR (0x0)]";
  61. }
  62. std::ostringstream oss;
  63. oss << "[Pointeur @" << static_cast<const void*>(ptr)
  64. << " pointant vers: " << *ptr << "]";
  65. return oss.str();
  66. }
  67. };
  68.  
  69. // 4. Spécialisation totale pour const char* (éviter de déférencer comme un simple char)
  70. template <>
  71. class Formateur<const char*> {
  72. public:
  73. static std::string formater(const char* texte) {
  74. if (texte == nullptr) return "[Chaîne C: NULL]";
  75. return "[Chaîne C: \"" + std::string(texte) + "\"]";
  76. }
  77. };
  78.  
  79. int main() {
  80. std::cout << "=== TD 3 - Exercice 3 : Spécialisation de Templates ===\n\n";
  81.  
  82. // --- 1. Test du patron de fonction plusGrand ---
  83. std::cout << "--- 1. Patron de fonction : Problématique de const char* ---\n";
  84. int n1 = 100, n2 = 250;
  85. std::cout << "plusGrand(100, 250) : " << plusGrand(n1, n2) << " (entiers standard)\n";
  86.  
  87. double d1 = 3.1415, d2 = 2.7182;
  88. std::cout << "plusGrand(3.1415, 2.7182) : " << plusGrand(d1, d2) << " (réels standard)\n";
  89.  
  90. // Chaînes C littérales
  91. const char* mot1 = "zoo";
  92. const char* mot2 = "arbre";
  93.  
  94. std::cout << "\nComparaison de deux chaînes C littérales (\"" << mot1 << "\" et \"" << mot2 << "\") :\n";
  95. std::cout << "Adresse de \"" << mot1 << "\" : " << static_cast<const void*>(mot1) << "\n";
  96. std::cout << "Adresse de \"" << mot2 << "\" : " << static_cast<const void*>(mot2) << "\n";
  97.  
  98. // Grâce à la spécialisation template <>, c'est std::strcmp qui est appelé :
  99. const char* gagnant = plusGrand(mot1, mot2);
  100. std::cout << "Résultat lexicographique via spécialisation : \"" << gagnant << "\" (correct !)\n";
  101.  
  102. // --- 2. Test du patron de classe Formateur ---
  103. std::cout << "\n--- 2. Patron de classe Formateur<T> et ses spécialisations ---\n";
  104.  
  105. // Cas générique : entiers, réels, string
  106. std::cout << Formateur<int>::formater(42) << "\n";
  107. std::cout << Formateur<double>::formater(19.99) << "\n";
  108. std::cout << Formateur<std::string>::formater(std::string("C++20")) << "\n";
  109.  
  110. // Cas spécialisé bool
  111. std::cout << Formateur<bool>::formater(true) << "\n";
  112. std::cout << Formateur<bool>::formater(false) << "\n";
  113.  
  114. // Cas spécialisé pour les pointeurs
  115. int variable = 777;
  116. int* ptrVar = &variable;
  117. int* ptrNul = nullptr;
  118.  
  119. std::cout << Formateur<int*>::formater(ptrVar) << "\n";
  120. std::cout << Formateur<int*>::formater(ptrNul) << "\n";
  121.  
  122. // Cas spécialisé pour const char*
  123. std::cout << Formateur<const char*>::formater("Licence Professionnelle Informatique") << "\n";
  124.  
  125. return 0;
  126. }

Résultat de l'exécution :

=== TD 3 - Exercice 3 : Spécialisation de Templates === --- 1. Patron de fonction : Problématique de const char* --- plusGrand(100, 250) : 250 (entiers standard) plusGrand(3.1415, 2.7182) : 3.1415 (réels standard) Comparaison de deux chaînes C littérales ("zoo" et "arbre") : Adresse de "zoo" : 0x5de6b601a14b Adresse de "arbre" : 0x5de6b601a14f Résultat lexicographique via spécialisation : "zoo" (correct !) --- 2. Patron de classe Formateur<T> et ses spécialisations --- [Générique: 42] [Générique: 19.99] [Générique: C++20] [Booléen: VRAI (TRUE)] [Booléen: FAUX (FALSE)] [Pointeur @0x7ffdbff98148 pointant vers: 777] [Pointeur: NULLPTR (0x0)] [Chaîne C: "Licence Professionnelle Informatique"]
Exercice 4

Conteneur Dynamique Générique & Règle des Trois : VecteurGenerique<T>

★★★★☆ Difficulté : Avancé ⏱️ 30 min Concepts : Class template, new[]/delete[], Règle des 3, Réallocation dynamique

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

  1. Définissez le patron de classe VecteurGenerique<typename T> contenant trois attributs privés : un pointeur T* m_donnees, une taille actuelle std::size_t m_taille et une capacité allouée std::size_t m_capacite.
  2. Écrivez le constructeur paramétré explicit VecteurGenerique(std::size_t capaciteInitiale = 2) qui alloue le tableau dynamique sur le tas.
  3. Règle des Trois (Pilier 1 - Destructeur) : Écrivez ~VecteurGenerique() libérant la ressource avec delete[] m_donnees.
  4. 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$.
  5. 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.
  6. 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.
  7. 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.
  8. Ajoutez pop_back(), operator[], at(), getTaille(), getCapacite() et afficher().
  9. Dans le main(), testez votre conteneur avec des int, puis avec des std::string. Vérifiez l'indépendance de mémoire lors d'une copie et testez l'auto-affectation v = v;.
💡 Stratégie de croissance : Pourquoi doubler la capacité ?

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).

CPPtd3_ex4_vecteur_generique.cpp
  1. /**
  2.  * TD 3 - Exercice 4 : Conteneur Dynamique Générique & Règle des Trois
  3.  * Implémentation complète d'un VecteurGenerique<T> auto-redimensionnable en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <stdexcept>
  9. #include <algorithm>
  10.  
  11. template <typename T>
  12. class VecteurGenerique {
  13. private:
  14. T* m_donnees;
  15. std::size_t m_taille;
  16. std::size_t m_capacite;
  17.  
  18. public:
  19. // 1. Constructeur par défaut (allocation minimale)
  20. explicit VecteurGenerique(std::size_t capaciteInitiale = 2)
  21. : m_donnees(capaciteInitiale > 0 ? new T[capaciteInitiale] : nullptr),
  22. m_taille(0),
  23. m_capacite(capaciteInitiale) {
  24. std::cout << " [+] VecteurGenerique(@" << this << ") alloué (capacité: "
  25. << m_capacite << ")\n";
  26. }
  27.  
  28. // 2. Destructeur (Pilier 1 de la Règle des Trois)
  29. ~VecteurGenerique() {
  30. std::cout << " [-] ~VecteurGenerique(@" << this << ") libération mémoire tas @"
  31. << static_cast<void*>(m_donnees) << "\n";
  32. delete[] m_donnees;
  33. }
  34.  
  35. // 3. Constructeur par recopie profonde (Pilier 2 de la Règle des Trois)
  36. VecteurGenerique(const VecteurGenerique<T>& source)
  37. : m_donnees(source.m_capacite > 0 ? new T[source.m_capacite] : nullptr),
  38. m_taille(source.m_taille),
  39. m_capacite(source.m_capacite) {
  40. for (std::size_t i = 0; i < m_taille; ++i) {
  41. m_donnees[i] = source.m_donnees[i]; // Recopie de chaque élément T
  42. }
  43. std::cout << " [C] VecteurGenerique COPIÉ depuis @" << &source
  44. << " vers @" << this << " (taille: " << m_taille << ")\n";
  45. }
  46.  
  47. // 4. Opérateur d'affectation sécurisé (Pilier 3 de la Règle des Trois)
  48. VecteurGenerique<T>& operator=(const VecteurGenerique<T>& source) {
  49. // Protection vitale contre l'auto-affectation (v = v)
  50. if (this == &source) {
  51. std::cout << " [=] Auto-affectation détectée sur @" << this << ", ignorée.\n";
  52. return *this;
  53. }
  54.  
  55. // Allocation préalable du nouveau tampon (sécurité aux exceptions)
  56. T* nouveauTampon = source.m_capacite > 0 ? new T[source.m_capacite] : nullptr;
  57. for (std::size_t i = 0; i < source.m_taille; ++i) {
  58. nouveauTampon[i] = source.m_donnees[i];
  59. }
  60.  
  61. // Libération de l'ancienne mémoire
  62. delete[] m_donnees;
  63.  
  64. // Mise à jour des métadonnées
  65. m_donnees = nouveauTampon;
  66. m_taille = source.m_taille;
  67. m_capacite = source.m_capacite;
  68.  
  69. std::cout << " [=] VecteurGenerique AFFECTÉ depuis @" << &source << " vers @" << this << "\n";
  70. return *this;
  71. }
  72.  
  73. // Redimensionnement automatique (stratégie géométrique par doublement)
  74. void reserver(std::size_t nouvelleCapacite) {
  75. if (nouvelleCapacite <= m_capacite) return;
  76.  
  77. T* nouveauTampon = new T[nouvelleCapacite];
  78. for (std::size_t i = 0; i < m_taille; ++i) {
  79. nouveauTampon[i] = m_donnees[i];
  80. }
  81.  
  82. delete[] m_donnees;
  83. m_donnees = nouveauTampon;
  84. m_capacite = nouvelleCapacite;
  85. std::cout << " -> Réallocation dynamique (@" << this << ") : capacité portée à "
  86. << m_capacite << "\n";
  87. }
  88.  
  89. // Ajout d'un élément en fin de conteneur
  90. void push_back(const T& valeur) {
  91. if (m_taille == m_capacite) {
  92. reserver(m_capacite == 0 ? 2 : m_capacite * 2);
  93. }
  94. m_donnees[m_taille++] = valeur;
  95. }
  96.  
  97. // Retrait du dernier élément
  98. void pop_back() {
  99. if (m_taille == 0) {
  100. throw std::underflow_error("Impossible de faire pop_back() sur un vecteur vide !");
  101. }
  102. --m_taille;
  103. }
  104.  
  105. // Accesseurs
  106. std::size_t getTaille() const noexcept { return m_taille; }
  107. std::size_t getCapacite() const noexcept { return m_capacite; }
  108. bool estVide() const noexcept { return m_taille == 0; }
  109.  
  110. // Accès aux éléments
  111. T& operator[](std::size_t index) noexcept { return m_donnees[index]; }
  112. const T& operator[](std::size_t index) const noexcept { return m_donnees[index]; }
  113.  
  114. T& at(std::size_t index) {
  115. if (index >= m_taille) {
  116. throw std::out_of_range("Index hors limites (" + std::to_string(index)
  117. + " >= " + std::to_string(m_taille) + ")");
  118. }
  119. return m_donnees[index];
  120. }
  121.  
  122. const T& at(std::size_t index) const {
  123. if (index >= m_taille) {
  124. throw std::out_of_range("Index hors limites (" + std::to_string(index)
  125. + " >= " + std::to_string(m_taille) + ")");
  126. }
  127. return m_donnees[index];
  128. }
  129.  
  130. void afficher(const std::string& nom) const {
  131. std::cout << nom << " [taille=" << m_taille << ", cap=" << m_capacite << "] : [ ";
  132. for (std::size_t i = 0; i < m_taille; ++i) {
  133. std::cout << m_donnees[i] << " ";
  134. }
  135. std::cout << "]\n";
  136. }
  137. };
  138.  
  139. int main() {
  140. std::cout << "=== TD 3 - Exercice 4 : VecteurGenerique<T> & Règle des Trois ===\n\n";
  141.  
  142. std::cout << "--- 1. Utilisation avec des entiers et redimensionnement dynamique ---\n";
  143. {
  144. VecteurGenerique<int> v(2);
  145. v.push_back(10);
  146. v.push_back(20);
  147. v.afficher("v après 2 ajouts");
  148.  
  149. std::cout << "Ajout d'un 3ème élément (déclenche réallocation) :\n";
  150. v.push_back(30);
  151. v.afficher("v après 3ème ajout");
  152.  
  153. v.push_back(40);
  154. v.push_back(50);
  155. v.afficher("v après 5 ajouts");
  156.  
  157. std::cout << "\n--- 2. Test du Constructeur de Recopie (copie profonde) ---\n";
  158. VecteurGenerique<int> copie = v;
  159. copie.afficher("copie");
  160.  
  161. std::cout << "Modification de copie[0] = 999...\n";
  162. copie[0] = 999;
  163. std::cout << "copie[0] = " << copie[0] << " | original v[0] = " << v[0]
  164. << " (indépendance mémoire vérifiée !)\n";
  165.  
  166. std::cout << "\n--- 3. Test de l'Affectation et de l'Auto-affectation ---\n";
  167. VecteurGenerique<int> autre(1);
  168. autre = v;
  169. autre.afficher("autre après affectation");
  170.  
  171. std::cout << "Test critique autre = autre :\n";
  172. autre = autre;
  173. autre.afficher("autre après auto-affectation");
  174.  
  175. std::cout << "\nSortie de bloc (destructions automatiques RAII) :\n";
  176. }
  177.  
  178. std::cout << "\n--- 4. Utilisation avec des chaînes de caractères (std::string) ---\n";
  179. {
  180. VecteurGenerique<std::string> mots;
  181. mots.push_back("C++");
  182. mots.push_back("Templates");
  183. mots.push_back("Généricité");
  184. mots.push_back("RAII");
  185. mots.afficher("mots");
  186. }
  187.  
  188. return 0;
  189. }

Résultat de l'exécution :

=== TD 3 - Exercice 4 : VecteurGenerique<T> & Règle des Trois === --- 1. Utilisation avec des entiers et redimensionnement dynamique --- [+] VecteurGenerique(@0x72f4221f0120) alloué (capacité: 2) v après 2 ajouts [taille=2, cap=2] : [ 10 20 ] Ajout d'un 3ème élément (déclenche réallocation) : -> Réallocation dynamique (@0x72f4221f0120) : capacité portée à 4 v après 3ème ajout [taille=3, cap=4] : [ 10 20 30 ] -> Réallocation dynamique (@0x72f4221f0120) : capacité portée à 8 v après 5 ajouts [taille=5, cap=8] : [ 10 20 30 40 50 ] --- 2. Test du Constructeur de Recopie (copie profonde) --- [C] VecteurGenerique COPIÉ depuis @0x72f4221f0120 vers @0x72f4221f0160 (taille: 5) copie [taille=5, cap=8] : [ 10 20 30 40 50 ] Modification de copie[0] = 999... copie[0] = 999 | original v[0] = 10 (indépendance mémoire vérifiée !) --- 3. Test de l'Affectation et de l'Auto-affectation --- [+] VecteurGenerique(@0x72f4221f01a0) alloué (capacité: 1) [=] VecteurGenerique AFFECTÉ depuis @0x72f4221f0120 vers @0x72f4221f01a0 autre après affectation [taille=5, cap=8] : [ 10 20 30 40 50 ] Test critique autre = autre : [=] Auto-affectation détectée sur @0x72f4221f01a0, ignorée. autre après auto-affectation [taille=5, cap=8] : [ 10 20 30 40 50 ] Sortie de bloc (destructions automatiques RAII) : [-] ~VecteurGenerique(@0x72f4221f01a0) libération mémoire tas @0x7324231e0130 [-] ~VecteurGenerique(@0x72f4221f0160) libération mémoire tas @0x7324231e0100 [-] ~VecteurGenerique(@0x72f4221f0120) libération mémoire tas @0x7324231e00a0 --- 4. Utilisation avec des chaînes de caractères (std::string) --- [+] VecteurGenerique(@0x72f4221f01e0) alloué (capacité: 2) -> Réallocation dynamique (@0x72f4221f01e0) : capacité portée à 4 mots [taille=4, cap=4] : [ C++ Templates Généricité RAII ] [-] ~VecteurGenerique(@0x72f4221f01e0) libération mémoire tas @0x73c4231e0048
Exercice 5

Structure Chaînée Générique : File d'Attente FIFO (Queue<T>)

★★★★★ Difficulté : Expert ⏱️ 35 min Concepts : Liste chaînée générique, FIFO, Noeud<T>, Règle des 3, Exceptions

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 par defiler().
  • m_queue (Back) : pointeur sur le dernier maillon inséré par enfiler().

2. Travail à réaliser

  1. Créez le patron de classe FileGenerique<typename T>. Définissez en section privée la structure imbriquée struct Noeud { T donnee; Noeud* suivant; ... };.
  2. Déclarez les attributs privés : Noeud* m_tete;, Noeud* m_queue; et std::size_t m_taille;.
  3. Écrivez le constructeur par défaut initialisant une file vide (pointeurs à nullptr et taille à 0).
  4. Écrivez la méthode void vider() qui parcourt la liste et libère individuellement chaque nœud avec delete. Utilisez cette méthode dans le destructeur ~FileGenerique().
  5. Implémentez void enfiler(const T& valeur) : alloue un nouveau nœud, l'ajoute derrière m_queue et met à jour les pointeurs. Vérifiez que l'opération s'effectue en temps constant $O(1)$.
  6. Implémentez T defiler() : extrait la valeur en tête, avance m_tete, libère le nœud extrait avec delete et renvoie la valeur. Si la file est vide, levez une exception std::underflow_error.
  7. Ajoutez les méthodes const T& premier() const, bool estVide() const noexcept et std::size_t getTaille() const noexcept.
  8. 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.
  9. 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.

⚡ Synthèse : Atouts d'une File Chaînée Générique

1
Complexité O(1)

Enfiler & Défiler

Ajout en queue et retrait en tête en temps strictement constant sans réallocation globale.

2
Zéro Sur-réservation

Allocation à la demande

La mémoire consommée est exactement proportionnelle au nombre de maillons actifs.

3
Sécurité Mémoire

Règle des Trois

Destructeur itératif et copie profonde empêchant toute libération multiple ou fuite.

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 :

CPPtd3_ex5_file_generique.cpp
  1. /**
  2.  * TD 3 - Exercice 5 : Structure de Données Chaînée Générique
  3.  * Conception intégrale d'une File<T> (Queue FIFO) en C++20 avec Règle des Trois.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <stdexcept>
  9.  
  10. template <typename T>
  11. class FileGenerique {
  12. private:
  13. // Structure interne représentant un maillon de la liste chaînée
  14. struct Noeud {
  15. T donnee;
  16. Noeud* suivant;
  17.  
  18. Noeud(const T& val) : donnee(val), suivant(nullptr) {}
  19. };
  20.  
  21. Noeud* m_tete; // Pointeur vers le premier élément à sortir (Tête / Front)
  22. Noeud* m_queue; // Pointeur vers le dernier élément entré (Queue / Back)
  23. std::size_t m_taille; // Nombre d'éléments présents dans la file
  24.  
  25. public:
  26. // 1. Constructeur par défaut (file initialement vide)
  27. FileGenerique() : m_tete(nullptr), m_queue(nullptr), m_taille(0) {
  28. std::cout << " [+] FileGenerique(@" << this << ") initialisée vide\n";
  29. }
  30.  
  31. // 2. Destructeur (libération de tous les nœuds chaînés)
  32. ~FileGenerique() {
  33. std::cout << " [-] ~FileGenerique(@" << this << ") destruction de "
  34. << m_taille << " nœud(s)...\n";
  35. vider();
  36. }
  37.  
  38. // 3. Constructeur par recopie (copie profonde de la liste chaînée)
  39. FileGenerique(const FileGenerique<T>& source)
  40. : m_tete(nullptr), m_queue(nullptr), m_taille(0) {
  41. Noeud* courant = source.m_tete;
  42. while (courant != nullptr) {
  43. enfiler(courant->donnee); // Reconstruit fidèlement les nœuds dans l'ordre FIFO
  44. courant = courant->suivant;
  45. }
  46. std::cout << " [C] FileGenerique COPIÉE depuis @" << &source
  47. << " vers @" << this << " (" << m_taille << " éléments)\n";
  48. }
  49.  
  50. // 4. Opérateur d'affectation sécurisé
  51. FileGenerique<T>& operator=(const FileGenerique<T>& source) {
  52. if (this == &source) {
  53. std::cout << " [=] Auto-affectation détectée sur @" << this << ", ignorée.\n";
  54. return *this;
  55. }
  56.  
  57. // Nettoyer les éléments actuels
  58. vider();
  59.  
  60. // Dupliquer les nœuds de la source
  61. Noeud* courant = source.m_tete;
  62. while (courant != nullptr) {
  63. enfiler(courant->donnee);
  64. courant = courant->suivant;
  65. }
  66.  
  67. std::cout << " [=] FileGenerique AFFECTÉE depuis @" << &source
  68. << " vers @" << this << "\n";
  69. return *this;
  70. }
  71.  
  72. // Vide complètement la file et désalloue les nœuds
  73. void vider() {
  74. while (m_tete != nullptr) {
  75. Noeud* aSupprimer = m_tete;
  76. m_tete = m_tete->suivant;
  77. delete aSupprimer;
  78. }
  79. m_queue = nullptr;
  80. m_taille = 0;
  81. }
  82.  
  83. // Enfile un élément en queue de file (Complexité O(1))
  84. void enfiler(const T& valeur) {
  85. Noeud* nouveau = new Noeud(valeur);
  86. if (estVide()) {
  87. m_tete = nouveau;
  88. m_queue = nouveau;
  89. } else {
  90. m_queue->suivant = nouveau;
  91. m_queue = nouveau;
  92. }
  93. ++m_taille;
  94. }
  95.  
  96. // Défile et renvoie le premier élément en tête de file (Complexité O(1))
  97. T defiler() {
  98. if (estVide()) {
  99. throw std::underflow_error("Erreur : Impossible de défiler une file vide !");
  100. }
  101.  
  102. Noeud* ancien = m_tete;
  103. T valeurSortie = ancien->donnee;
  104.  
  105. m_tete = m_tete->suivant;
  106. if (m_tete == nullptr) {
  107. m_queue = nullptr; // La file est devenue complètement vide
  108. }
  109.  
  110. delete ancien;
  111. --m_taille;
  112. return valeurSortie;
  113. }
  114.  
  115. // Consultation du premier élément sans le retirer
  116. const T& premier() const {
  117. if (estVide()) {
  118. throw std::underflow_error("File vide : aucun élément en tête !");
  119. }
  120. return m_tete->donnee;
  121. }
  122.  
  123. bool estVide() const noexcept {
  124. return m_tete == nullptr;
  125. }
  126.  
  127. std::size_t getTaille() const noexcept {
  128. return m_taille;
  129. }
  130.  
  131. // Affichage de la file du premier sorti vers le dernier entré
  132. void afficher(const std::string& nom) const {
  133. std::cout << nom << " [taille=" << m_taille << "] (Tête -> Queue) : [ ";
  134. Noeud* courant = m_tete;
  135. while (courant != nullptr) {
  136. std::cout << courant->donnee;
  137. if (courant->suivant != nullptr) std::cout << " -> ";
  138. courant = courant->suivant;
  139. }
  140. std::cout << " ]\n";
  141. }
  142. };
  143.  
  144. int main() {
  145. std::cout << "=== TD 3 - Exercice 5 : FileGenerique<T> (Queue FIFO Chaînée) ===\n\n";
  146.  
  147. std::cout << "--- 1. File de tâches d'impression (std::string) ---\n";
  148. {
  149. FileGenerique<std::string> spooler;
  150. spooler.enfiler("Doc_Compta.pdf");
  151. spooler.enfiler("Rapport_Stage.docx");
  152. spooler.enfiler("Photo_Identite.png");
  153.  
  154. spooler.afficher("Spooler d'impression");
  155. std::cout << "Prochain document à imprimer : \"" << spooler.premier() << "\"\n\n";
  156.  
  157. std::cout << "Traitement (défilement FIFO) :\n";
  158. std::cout << "-> Impression en cours : " << spooler.defiler() << "\n";
  159. std::cout << "-> Impression en cours : " << spooler.defiler() << "\n";
  160. spooler.afficher("Spooler restant");
  161.  
  162. std::cout << "\nAjout d'une nouvelle tâche urgente :\n";
  163. spooler.enfiler("Facture_Client.pdf");
  164. spooler.afficher("Spooler mis à jour");
  165.  
  166. std::cout << "\n--- 2. Test du Constructeur de Recopie (indépendance des listes) ---\n";
  167. FileGenerique<std::string> copieSpooler = spooler;
  168. copieSpooler.afficher("Copie du spooler");
  169.  
  170. std::cout << "Défilement sur la copie : " << copieSpooler.defiler() << "\n";
  171. copieSpooler.afficher("Copie après défilement");
  172. spooler.afficher("Original (intact !)");
  173.  
  174. std::cout << "\n--- 3. Affectation et Auto-affectation ---\n";
  175. FileGenerique<std::string> autre;
  176. autre = spooler;
  177. autre.afficher("Autre après affectation");
  178.  
  179. autre = autre; // Test auto-affectation
  180. autre.afficher("Autre après auto-affectation");
  181.  
  182. std::cout << "\nSortie de bloc : libération mémoire nœud par nœud...\n";
  183. }
  184.  
  185. std::cout << "\n--- 4. File d'entiers & Gestion des Exceptions ---\n";
  186. {
  187. FileGenerique<int> fileEntiers;
  188. fileEntiers.enfiler(100);
  189. fileEntiers.enfiler(200);
  190.  
  191. std::cout << "Élément retiré : " << fileEntiers.defiler() << "\n";
  192. std::cout << "Élément retiré : " << fileEntiers.defiler() << "\n";
  193.  
  194. try {
  195. std::cout << "Tentative de défiler une file vide...\n";
  196. fileEntiers.defiler(); // Lève underflow_error
  197. } catch (const std::underflow_error& e) {
  198. std::cout << ">> Exception interceptée avec succès : " << e.what() << "\n";
  199. }
  200. }
  201.  
  202. return 0;
  203. }

Résultat de l'exécution :

=== TD 3 - Exercice 5 : FileGenerique<T> (Queue FIFO Chaînée) === --- 1. File de tâches d'impression (std::string) --- [+] FileGenerique(@0x79bbfeff0100) initialisée vide Spooler d'impression [taille=3] (Tête -> Queue) : [ Doc_Compta.pdf -> Rapport_Stage.docx -> Photo_Identite.png ] Prochain document à imprimer : "Doc_Compta.pdf" Traitement (défilement FIFO) : -> Impression en cours : Doc_Compta.pdf -> Impression en cours : Rapport_Stage.docx Spooler restant [taille=1] (Tête -> Queue) : [ Photo_Identite.png ] Ajout d'une nouvelle tâche urgente : Spooler mis à jour [taille=2] (Tête -> Queue) : [ Photo_Identite.png -> Facture_Client.pdf ] --- 2. Test du Constructeur de Recopie (indépendance des listes) --- [C] FileGenerique COPIÉE depuis @0x79bbfeff0100 vers @0x79bbfeff0140 (2 éléments) Copie du spooler [taille=2] (Tête -> Queue) : [ Photo_Identite.png -> Facture_Client.pdf ] Défilement sur la copie : Photo_Identite.png Copie après défilement [taille=1] (Tête -> Queue) : [ Facture_Client.pdf ] Original (intact !) [taille=2] (Tête -> Queue) : [ Photo_Identite.png -> Facture_Client.pdf ] --- 3. Affectation et Auto-affectation --- [+] FileGenerique(@0x79bbfeff0180) initialisée vide [=] FileGenerique AFFECTÉE depuis @0x79bbfeff0100 vers @0x79bbfeff0180 Autre après affectation [taille=2] (Tête -> Queue) : [ Photo_Identite.png -> Facture_Client.pdf ] [=] Auto-affectation détectée sur @0x79bbfeff0180, ignorée. Autre après auto-affectation [taille=2] (Tête -> Queue) : [ Photo_Identite.png -> Facture_Client.pdf ] Sortie de bloc : libération mémoire nœud par nœud... [-] ~FileGenerique(@0x79bbfeff0180) destruction de 2 nœud(s)... [-] ~FileGenerique(@0x79bbfeff0140) destruction de 1 nœud(s)... [-] ~FileGenerique(@0x79bbfeff0100) destruction de 2 nœud(s)... --- 4. File d'entiers & Gestion des Exceptions --- [+] FileGenerique(@0x79bbfeff01c0) initialisée vide Élément retiré : 100 Élément retiré : 200 Tentative de défiler une file vide... >> Exception interceptée avec succès : Erreur : Impossible de défiler une file vide ! [-] ~FileGenerique(@0x79bbfeff01c0) destruction de 0 nœud(s)...

🎉 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.
← Revoir le Chapitre 3 (Généricité) ← Revoir le TD 2 (Héritage) Passer au Chapitre 4 : La STL C++ ➔ Accueil du Cours ➔