Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 1h20 (CM/TD) Coloration : GeSHi

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

L'Intérêt de la Généricité : Pourquoi et Comment ?

⏱️ 15 minutes

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

💡 Analogie : Le moule industriel

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

Les Patrons de Fonctions (Function Templates)

⏱️ 20 minutes

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

CPPfonctions_generiques.cpp
#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.

Mécanisme d'Instanciation à la Compilation
📜 Code Source Template
template <typename T> T minimum(T a, T b)
Compilation ➔
⚙️ Fonctions Réelles Générées
  • 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
3

Patrons de Classes et Conteneurs Génériques

⏱️ 25 minutes

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>

CPPPaire.hpp
#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** :

CPPPile.hpp
#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 :

CPPTableauFixe.hpp
#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
4

Organisation des Fichiers et Pièges Classiques

⏱️ 10 minutes
🚨 Le Piège N°1 en C++ : La séparation .hpp / .cpp des Templates

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 type T doit obligatoirement définir l'operator<.
  • Si le conteneur alloue new T[N], le type T doit 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
5

Synthèse & Auto-évaluation

⏱️ 10 minutes

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)

❓ Question 1 : Pourquoi l'utilisation des templates en C++ n'entraîne-t-elle généralement aucun surcoût de performance à l'exécution par rapport à du code non générique ?
A. Parce que les templates utilisent une machine virtuelle optimisée comme la JVM.
B. Parce que le compilateur génère à la compilation du code machine natif spécifique et optimisé pour chaque type utilisé (monomorphisation).
C. Parce que tous les types sont convertis dynamiquement en pointeurs génériques lors de l'exécution.
Réponse B : En C++, les templates sont résolus intégralement à la compilation. Le compilateur produit pour chaque combinaison de types un code machine binaire direct, sans couche d'indirection ni vérification dynamique à l'exécution (Zero-Cost Abstraction).
❓ Question 2 : Que se passe-t-il si vous implémentez les méthodes d'une classe template Vector<T> dans un fichier Vector.cpp compilé séparément, sans inclusion dans le .hpp ?
A. Le programme fonctionne parfaitement et se compile plus rapidement.
B. L'éditeur de liens (Linker) renvoie une erreur d'édition de liens (undefined reference) dans les fichiers qui utilisent Vector<int>.
C. Le compilateur signale un avertissement mais génère le binaire correctement.
Réponse B : Lorsque 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.
❓ Question 3 : Si une fonction template 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 ?
A. Le code compile mais lève une exception à l'exécution.
B. Le compilateur refuse de compiler et signale une erreur de type indiquant que l'opérateur '<' n'est pas défini pour Complexe.
C. Le compilateur compare par défaut les adresses mémoire des deux objets.
Réponse B : C'est la force du typage statique des templates en C++ : la vérification sémantique a lieu dès la phase d'instanciation à la compilation. Si une opération requise par le template n'est pas supportée par le type 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> contenant T m_valeur; Noeud<T>* m_suivant;.
  • Méthodes enfiler(const T& val) et T defiler().
  • Gestion propre de la mémoire dans le destructeur et le constructeur par recopie.
📝 Ouvrir le TD 3 complet (Exercice 5 détaillé & 4 autres exercices) ➔

Suite du cours

Après avoir assimilé les principes de conception des patrons génériques, découvrez la bibliothèque standard C++ (STL) :

Pratiquer sur le TD 3 (Templates) ➔ Passer au Chapitre 4 : La STL C++ ➔