Module 07 : Templates C++
Concepts clés :
- Templates de fonctions
- Templates de classes
- Instanciation de templates
- Spécialisation de templates
Pourquoi c’est important
Section intitulée « Pourquoi c’est important »Les templates permettent la programmation générique — écrire du code une fois qui fonctionne avec n’importe quel type de données. Le compilateur génère des versions spécifiques selon les besoins.
Les templates alimentent toute la Standard Template Library (STL) — chaque vector<T>, map<K,V> et algorithme utilise des templates.
Pourquoi les templates ? (Contexte historique)
Section intitulée « Pourquoi les templates ? (Contexte historique) »Avant les templates C++, les programmeurs C utilisaient des macros paramétriques pour écrire du code générique :
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int result = MAX(3, 5); // Fonctionnedouble d = MAX(3.14, 2.71); // FonctionneMais les macros ont de sérieux problèmes :
- Pas de sécurité de type :
MAX("hello", 42)compile mais n’a aucun sens - Bugs de substitution de texte :
MAX(i++, j)incrémenteideux fois sii > j - Pas de portée : Les macros polluent l’espace de noms global
- Pas de débogage : Les erreurs pointent vers le code expansé, pas vers la macro
Les templates résolvent tous ces problèmes. Le compilateur :
- Vérifie les types à la compilation
- Génère de vraies fonctions (pas de substitution de texte)
- Respecte les règles de portée
- Fournit des messages d’erreur vérifiés par type (bien que verbeux)
Le modèle mental « Code avec des trous »
Section intitulée « Le modèle mental « Code avec des trous » »Pensez aux templates comme des patrons de code avec des trous (placeholders) que le compilateur remplit :
template <typename T> // "T" est le trouT max(T a, T b) { // Patron avec des trous return (a > b) ? a : b;}
max(3, 5); // Le compilateur remplit le trou avec "int"max(3.14, 2.71); // Le compilateur remplit le trou avec "double"Quand vous utilisez un template, le compilateur l’instancie : il prend votre patron, remplit les trous avec les types réels et génère du vrai code. C’est de la génération de code à la compilation, pas du polymorphisme à l’exécution.
Comprendre la syntaxe des templates
Section intitulée « Comprendre la syntaxe des templates »Ce module introduit les templates. Voici la nouvelle syntaxe expliquée :
Le mot-clé template <>
Section intitulée « Le mot-clé template <> »template <typename T>T max(T a, T b) { return (a > b) ? a : b; }template <>déclare que ce qui suit est un templatetypename T(ouclass T) déclare un paramètre de type appeléTTpeut être n’importe quel type :int,double,std::string, votre propre classe, etc.- Quand vous utilisez le template, le compilateur génère du code pour le type spécifique
typenameetclasssont interchangeables en C++98 (les deux signifient la même chose)
Les chevrons <> (Paramètres de template)
Section intitulée « Les chevrons <> (Paramètres de template) »max<int>(3, 5); // Utiliser explicitement intmax(3.14, 2.71); // Le compilateur déduit doubleArray<int> intArray; // Instanciation de template de classe<>entourent les arguments de template- Pour les fonctions, vous pouvez laisser le compilateur déduire le type des arguments
- Pour les classes, vous devez spécifier explicitement le type
- Lisez
Array<int>comme « Array de int »
Le :: de portée avec les templates
Section intitulée « Le :: de portée avec les templates »std::vector<int>::iterator it;::avec les templates accède aux types imbriquésvector<int>::iteratorsignifie « le type iterator à l’intérieur de vector-de-int »- Les classes template peuvent avoir leurs propres types imbriqués (types qui dépendent du paramètre template)
Le mot-clé typename (Désambiguïsateur)
Section intitulée « Le mot-clé typename (Désambiguïsateur) »typename std::vector<T>::iterator it; // Dire au compilateur que c'est un typetypenameavantClassName::NestedTypedit au compilateur « ceci est un type, pas une valeur »- À l’intérieur des templates, le compilateur ne sait pas si
vector<T>::iteratorest un type ou un membre statique typenamesupprime cette ambiguïté - il dit « fais-moi confiance, c’est un nom de type »- Requis lors de l’accès aux types imbriqués de classes dépendantes de template
Paramètres de template multiples
Section intitulée « Paramètres de template multiples »template <typename T, typename U>T convert(U value) { return static_cast<T>(value); }- Vous pouvez avoir plusieurs paramètres de template séparés par des virgules
Test le type de retour,Uest le type d’entrée- Usage :
convert<int>(3.14)spécifie explicitement T, le compilateur déduit U
Paramètres de template par défaut
Section intitulée « Paramètres de template par défaut »template <typename T = int>class Stack { };
Stack<> intStack; // Utilise le défaut : intStack<double> dblStack; // Explicite : double= intfournit un paramètre de type par défaut- Si non spécifié, le défaut est utilisé
- Utilisez
<>quand vous voulez le défaut (pas justeStack)
1. Pourquoi les templates ?
Section intitulée « 1. Pourquoi les templates ? »Le problème : Duplication du code
Section intitulée « Le problème : Duplication du code »// Sans templates - doit écrire pour chaque typeint maxInt(int a, int b) { return (a > b) ? a : b; }double maxDouble(double a, double b) { return (a > b) ? a : b; }std::string maxString(std::string a, std::string b) { return (a > b) ? a : b; }La solution : Templates
Section intitulée « La solution : Templates »// Avec templates - une définition, fonctionne pour tous les typestemplate <typename T>T max(T a, T b) { return (a > b) ? a : b;}
// Usagemax(3, 5); // Instancie max<int>max(3.14, 2.71); // Instancie max<double>max(str1, str2); // Instancie max<std::string>2. Templates de fonctions
Section intitulée « 2. Templates de fonctions »Syntaxe de base
Section intitulée « Syntaxe de base »template <typename T>T functionName(T param1, T param2) { // T peut être utilisé comme n'importe quel type return param1 + param2;}Paramètres de type multiples
Section intitulée « Paramètres de type multiples »template <typename T, typename U>T convert(U value) { return static_cast<T>(value);}
// Usageint i = convert<int>(3.14); // T explicite, U déduitTemplate vs typename
Section intitulée « Template vs typename »template <typename T> // Style modernetemplate <class T> // Aussi valide, signifie la même chose3. Module 07 Exercice 00 : swap, min, max
Section intitulée « 3. Module 07 Exercice 00 : swap, min, max »// swap : Échanger les valeurs de deux variablestemplate <typename T>void swap(T& a, T& b) { T temp = a; a = b; b = temp;}
// min : Retourner la plus petite valeur (retourner le second si égal)template <typename T>T const& min(T const& a, T const& b) { return (a < b) ? a : b;}
// max : Retourner la plus grande valeur (retourner le second si égal)template <typename T>T const& max(T const& a, T const& b) { return (a > b) ? a : b;}Code de test
Section intitulée « Code de test »int main() { int a = 2; int b = 3;
::swap(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; std::cout << "min(a, b) = " << ::min(a, b) << std::endl; std::cout << "max(a, b) = " << ::max(a, b) << std::endl;
std::string c = "chaine1"; std::string d = "chaine2";
::swap(c, d); std::cout << "c = " << c << ", d = " << d << std::endl; std::cout << "min(c, d) = " << ::min(c, d) << std::endl; std::cout << "max(c, d) = " << ::max(c, d) << std::endl;
return 0;}4. Module 07 Exercice 01 : iter
Section intitulée « 4. Module 07 Exercice 01 : iter »Pointeurs de fonction comme paramètres de template
Section intitulée « Pointeurs de fonction comme paramètres de template »// iter : Appliquer une fonction à chaque élément d'un tableau// F peut être un pointeur de fonction OU un type de foncteurtemplate <typename T, typename F>void iter(T* array, size_t length, F func) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Version explicite avec pointeur de fonction
Section intitulée « Version explicite avec pointeur de fonction »// Plus explicite : F est spécifiquement un pointeur de fonctiontemplate <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Surcharge de template : const vs non-const
Section intitulée « Surcharge de template : const vs non-const »Le même template peut être surchargé pour les opérations const et non-const :
// Version non-const - peut modifier les élémentstemplate <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}
// Version const - opérations en lecture seuletemplate <typename T>void iter(T* array, size_t length, void (*func)(T const&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Pourquoi les deux versions ?
Section intitulée « Pourquoi les deux versions ? »// Fonction qui modifie (nécessite référence non-const)template <typename T>void increment(T& elem) { elem++;}
// Fonction qui ne fait que lire (utilise référence const)template <typename T>void print(T const& elem) { std::cout << elem << std::endl;}
int arr[] = {1, 2, 3};
// Appelle la version non-constiter(arr, 3, increment<int>); // arr est maintenant {2, 3, 4}
// Appelle la version constiter(arr, 3, print<int>); // Affiche les élémentsSyntaxe de pointeur de fonction avec templates
Section intitulée « Syntaxe de pointeur de fonction avec templates »// En appelant avec une fonction template, vous devez l'instancier :iter(arr, 3, print<int>); // Argument de template expliciteiter(arr, 3, &print<int>); // & est optionnel
// Avec des fonctions non-template :void printInt(int const& x) { std::cout << x; }iter(arr, 3, printInt); // Pas besoin de <>Code de test
Section intitulée « Code de test »template <typename T>void print(T const& elem) { std::cout << elem << std::endl;}
template <typename T>void increment(T& elem) { elem++;}
int main() { int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original:" << std::endl; iter(arr, 5, print<int>);
iter(arr, 5, increment<int>);
std::cout << "After increment:" << std::endl; iter(arr, 5, print<int>);
return 0;}5. Templates de classes
Section intitulée « 5. Templates de classes »Syntaxe de base
Section intitulée « Syntaxe de base »template <typename T>class Container {private: T* _data; size_t _size;
public: Container(size_t size); Container(const Container& other); Container& operator=(const Container& other); ~Container();
T& operator[](size_t index); const T& operator[](size_t index) const; size_t size() const;};Implémentation
Section intitulée « Implémentation »// Pour les templates de classes, l'implémentation DOIT être dans le header// Ou dans un fichier .tpp inclus par le header
template <typename T>Container<T>::Container(size_t size) : _size(size) { _data = new T[size](); // () pour l'initialisation par valeur}
template <typename T>Container<T>::~Container() { delete[] _data;}
template <typename T>T& Container<T>::operator[](size_t index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _data[index];}6. Module 07 Exercice 02 : Array
Section intitulée « 6. Module 07 Exercice 02 : Array »template <typename T>class Array {private: T* _array; unsigned int _size;
public: // Constructeur par défaut - tableau vide Array() : _array(NULL), _size(0) {}
// Constructeur de taille - tableau de n éléments Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Constructeur de copie - copie profonde Array(const Array& other) : _array(NULL), _size(0) { *this = other; }
// Opérateur d'assignation - copie profonde Array& operator=(const Array& other) { if (this != &other) { delete[] _array; _size = other._size; _array = new T[_size]; for (unsigned int i = 0; i < _size; i++) _array[i] = other._array[i]; } return *this; }
// Destructeur ~Array() { delete[] _array; }
// Accès aux éléments avec vérification des limites T& operator[](unsigned int index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index]; }
const T& operator[](unsigned int index) const { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index]; }
// Getter de taille unsigned int size() const { return _size; }};Exception std::out_of_range
Section intitulée « Exception std::out_of_range »#include <stdexcept> // pour std::out_of_range
// out_of_range est utilisée pour les erreurs d'index/limitesT& operator[](unsigned int index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index];}
// Usage :try { Array<int> arr(5); arr[10] = 42; // Lance out_of_range}catch (std::out_of_range& e) { std::cout << "Exception: " << e.what() << std::endl;}catch (std::exception& e) { // Attrape n'importe quelle exception standard std::cout << "Error: " << e.what() << std::endl;}Hiérarchie des exceptions pour référence
Section intitulée « Hiérarchie des exceptions pour référence »std::exception├── std::logic_error│ ├── std::out_of_range <- Utiliser pour erreurs d'index│ ├── std::invalid_argument <- Utiliser pour mauvais arguments de fonction│ └── std::length_error <- Utiliser pour erreurs de limite de taille└── std::runtime_error <- Utiliser pour erreurs d'exécution généralesCode de test
Section intitulée « Code de test »int main() { // Tester constructeur par défaut Array<int> empty; std::cout << "Empty size: " << empty.size() << std::endl;
// Tester constructeur de taille Array<int> arr(5); std::cout << "Size: " << arr.size() << std::endl;
// Tester accès aux éléments for (unsigned int i = 0; i < arr.size(); i++) arr[i] = i * 2;
// Tester copie Array<int> copy(arr); arr[0] = 100; std::cout << "arr[0]: " << arr[0] << std::endl; std::cout << "copy[0]: " << copy[0] << std::endl; // Devrait être 0
// Tester vérification des limites try { arr[100] = 42; } catch (std::exception& e) { std::cout << "Exception: " << e.what() << std::endl; }
return 0;}7. Instanciation de templates
Section intitulée « 7. Instanciation de templates »Instanciation implicite
Section intitulée « Instanciation implicite »// Le compilateur génère du code quand le template est utiliséArray<int> intArray; // Génère Array<int>Array<double> dblArray; // Génère Array<double>Instanciation explicite
Section intitulée « Instanciation explicite »// Forcer la génération de types spécifiques (rarement nécessaire)template class Array<int>;template void swap<double>(double&, double&);8. Où mettre le code des templates
Section intitulée « 8. Où mettre le code des templates »Option 1 : Tout dans le header (.hpp)
Section intitulée « Option 1 : Tout dans le header (.hpp) »template <typename T>class Array { // Déclaration ET implémentation ici};Option 2 : Fichier .tpp séparé
Section intitulée « Option 2 : Fichier .tpp séparé »template <typename T>class Array { // Déclaration seulement};
#include "Array.tpp" // Inclure l'implémentation à la fin
// Array.tpptemplate <typename T>Array<T>::Array() { /* ... */ }La convention de nommage .tpp : Utiliser .tpp (template implementation) au lieu de .cpp identifie clairement les fichiers d’implémentation de templates dans votre codebase. Quand vous voyez un fichier .tpp, vous savez immédiatement qu’il contient des définitions de templates qui seront incluses dans un header.
Pourquoi pas .cpp ?
Section intitulée « Pourquoi pas .cpp ? »Les templates doivent être visibles au point d’instanciation. Si l’implémentation est dans un .cpp, le compilateur ne peut pas générer le code.
9. Spécialisation de templates
Section intitulée « 9. Spécialisation de templates »Parfois vous avez besoin d’un comportement différent pour des types spécifiques. La spécialisation de templates vous permet de fournir des implémentations personnalisées pour des arguments de template particuliers.
Spécialisation complète (Fonctions et classes)
Section intitulée « Spécialisation complète (Fonctions et classes) »// Template génériquetemplate <typename T>void print(T value) { std::cout << value << std::endl;}
// Spécialisation complète pour booltemplate <>void print<bool>(bool value) { std::cout << (value ? "true" : "false") << std::endl;}
// Usageprint(42); // Utilise générique : affiche "42"print(3.14); // Utilise générique : affiche "3.14"print(true); // Utilise spécialisation : affiche "true"Spécialisation de template de classe
Section intitulée « Spécialisation de template de classe »// Template génériquetemplate <typename T>class Storage { T _data;public: void store(T val) { _data = val; } T retrieve() { return _data; }};
// Spécialisation complète pour char*template <>class Storage<char*> { std::string _data; // Stocker comme string pour la sécuritépublic: void store(char* val) { _data = val; } const char* retrieve() { return _data.c_str(); }};Spécialisation partielle (Classes seulement)
Section intitulée « Spécialisation partielle (Classes seulement) »La spécialisation partielle fournit un template pour un sous-ensemble de types (ex : tous les pointeurs) :
// Template génériquetemplate <typename T>class Container {public: void info() { std::cout << "Generic container" << std::endl; }};
// Spécialisation partielle pour tous les types pointeurstemplate <typename T>class Container<T*> {public: void info() { std::cout << "Pointer container" << std::endl; }};
// UsageContainer<int> c1; // Utilise génériqueContainer<int*> c2; // Utilise spécialisation pointeurContainer<double*> c3; // Utilise spécialisation pointeurNote : Les templates de fonctions ne peuvent pas être partiellement spécialisés. Si vous avez besoin d’un comportement de spécialisation partielle pour les fonctions, utilisez la surcharge ou un template de classe auxiliaire.
Quand utiliser la spécialisation
Section intitulée « Quand utiliser la spécialisation »- Optimisation : Fournir des implémentations plus rapides pour des types spécifiques
- Comportement spécial : Gérer des types qui ne fonctionnent pas avec l’implémentation générique
- Traits de type : Construire des informations de type à la compilation (comme
is_pointerci-dessus)
10. Débogage des erreurs de templates
Section intitulée « 10. Débogage des erreurs de templates »Les messages d’erreur de templates sont notoirement difficiles à lire. Une petite erreur peut générer des pages d’erreurs. C’est un défi connu avec les templates, pas un échec personnel.
Conseils pour le débogage
Section intitulée « Conseils pour le débogage »-
Lire de bas en haut : L’erreur réelle est souvent vers la fin ; les messages précédents montrent le contexte d’instanciation
-
Chercher « instantiated from » : Cela vous dit OÙ le template a été utilisé, vous aidant à tracer le problème
-
Tester d’abord avec des types concrets : Si votre template ne fonctionne pas, essayez d’écrire le code pour un type spécifique (comme
int) d’abord -
Vérifier les opérations requises : Si votre template utilise
<, le type doit supporteroperator<
// Ce template requiert que operator< existe pour le type Ttemplate <typename T>T min(T a, T b) { return (a < b) ? a : b;}
// Ceci échouera avec une erreur confuse :struct NoCompare { int x; };NoCompare a, b;min(a, b); // Erreur : pas d'operator< pour NoCompare- Simplifier : Commenter des parties de votre template pour isoler le problème
11. Patrons de templates courants
Section intitulée « 11. Patrons de templates courants »Traits de type (style C++98)
Section intitulée « Traits de type (style C++98) »template <typename T>struct is_pointer { static const bool value = false;};
template <typename T>struct is_pointer<T*> { static const bool value = true;};Paramètres de template par défaut
Section intitulée « Paramètres de template par défaut »template <typename T = int>class Stack { // Par défaut int si aucun type spécifié};
Stack<> intStack; // Utilise défaut : intStack<double> dblStack; // Explicite : doubleExercice 00 : Commencer avec quelques fonctions
Section intitulée « Exercice 00 : Commencer avec quelques fonctions »Analyse du sujet
Section intitulée « Analyse du sujet »Implémenter trois templates de fonctions :
swap(a, b)- échanger deux valeursmin(a, b)- retourner la plus petite valeurmax(a, b)- retourner la plus grande valeur
Contraintes clés :
- Doit fonctionner avec n’importe quel type comparable
- Quand les valeurs sont égales, retourner le second argument
- Doit fonctionner avec le main de test fourni
Main de test fourni :
int main() { int a = 2, b = 3;
::swap(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; std::cout << "min(a, b) = " << ::min(a, b) << std::endl; std::cout << "max(a, b) = " << ::max(a, b) << std::endl;
std::string c = "chaine1", d = "chaine2";
::swap(c, d); std::cout << "c = " << c << ", d = " << d << std::endl; std::cout << "min(c, d) = " << ::min(c, d) << std::endl; std::cout << "max(c, d) = " << ::max(c, d) << std::endl;
return 0;}Stratégie d’approche
Section intitulée « Stratégie d’approche »Réfléchir avant de coder :
-
Qu’est-ce qu’un template ?
- Un modèle pour créer des fonctions/classes
- Le compilateur génère du vrai code quand vous l’utilisez
template <typename T>déclare un paramètre de type
-
Pourquoi utiliser
::swapau lieu deswap?::signifie espace de noms global- Évite le conflit avec
std::swap - Votre fonction doit être dans la portée globale
-
Pourquoi retourner une référence pour min/max ?
- Efficacité (éviter les copies)
- Exigence du sujet pour le cas d’égalité
- Retourner une référence vers le second argument quand égal
-
Quelles opérations T doit-il supporter ?
swap: assignation (=)min: comparaison inférieur (<)max: comparaison supérieur (>)
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 : Template swap de base
#ifndef WHATEVER_HPP#define WHATEVER_HPP
template <typename T>void swap(T& a, T& b) { T temp = a; // T doit supporter la construction par copie a = b; // T doit supporter l'assignation b = temp;}
#endifÉtape 2 : Ajouter min avec le bon comportement d’égalité
template <typename T>T const& min(T const& a, T const& b) { // Quand égal, retourner b (second argument) return (a < b) ? a : b;}Étape 3 : Ajouter max avec le bon comportement d’égalité
template <typename T>T const& max(T const& a, T const& b) { // Quand égal, retourner b (second argument) return (a > b) ? a : b;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »Décomposition de la syntaxe template :
| Syntaxe | Signification |
|---|---|
template <typename T> | Déclare T comme type placeholder |
T& a | Référence vers T (modifiable) |
T const& a | Référence const vers T (lecture seule) |
T const& min(...) | Retourne référence const vers T |
Pourquoi des références const ?
// Par valeur - MAUVAIS : copie les deux argumentsT min(T a, T b) { return (a < b) ? a : b; }
// Par référence const - BON : pas de copiesT const& min(T const& a, T const& b) { return (a < b) ? a : b; }Le cas d’égalité :
// Quand a == b, (a < b) est faux, donc retourne breturn (a < b) ? a : b; // Retourne b quand égal
// Quand a == b, (a > b) est faux, donc retourne breturn (a > b) ? a : b; // Retourne b quand égalPièges courants
Section intitulée « Pièges courants »1. Retour par valeur au lieu de référence :
// FAUX : Copie et perd le comportement "retourner le second quand égal"template <typename T>T min(T a, T b) { return (a < b) ? a : b; }
// CORRECT : Retourne référence vers l'argument réeltemplate <typename T>T const& min(T const& a, T const& b) { return (a < b) ? a : b; }2. Mauvaise condition pour le cas d’égalité :
// FAUX : Retourne le premier quand égalreturn (a <= b) ? a : b;
// CORRECT : Retourne le second quand égalreturn (a < b) ? a : b;3. Pas dans l’espace de noms global :
// FAUX : Dans un namespace, ::swap ne le trouvera pasnamespace MyLib { template <typename T> void swap(T& a, T& b) { ... }}
// CORRECT : Dans la portée globaletemplate <typename T>void swap(T& a, T& b) { ... }4. Utiliser les mauvais types de paramètres pour swap :
// FAUX : Par valeur - échange des copies, originaux inchangéstemplate <typename T>void swap(T a, T b) { T temp = a; a = b; b = temp;}
// CORRECT : Par référence - modifie les valeurs originalestemplate <typename T>void swap(T& a, T& b) { T temp = a; a = b; b = temp;}Conseils de test
Section intitulée « Conseils de test »#include <iostream>#include <string>#include "whatever.hpp"
int main() { // Tester avec int int a = 2, b = 3; std::cout << "Before: a=" << a << ", b=" << b << std::endl; ::swap(a, b); std::cout << "After swap: a=" << a << ", b=" << b << std::endl; std::cout << "min: " << ::min(a, b) << std::endl; std::cout << "max: " << ::max(a, b) << std::endl;
// Tester avec string std::string c = "chaine1", d = "chaine2"; ::swap(c, d); std::cout << "c=" << c << ", d=" << d << std::endl; std::cout << "min: " << ::min(c, d) << std::endl; std::cout << "max: " << ::max(c, d) << std::endl;
// Tester cas égalité - devrait retourner le second int x = 5, y = 5; std::cout << "Equal test: &x=" << &x << ", &y=" << &y << std::endl; std::cout << "min returns: " << &(::min(x, y)) << std::endl; std::cout << "max returns: " << &(::max(x, y)) << std::endl; // Devrait afficher &y pour les deux !
return 0;}Sortie attendue :
Before: a=2, b=3After swap: a=3, b=2min: 2max: 3c=chaine2, d=chaine1min: chaine1max: chaine2Equal test: &x=0x7fff..., &y=0x7fff...min returns: 0x7fff... (même que &y)max returns: 0x7fff... (même que &y)Code final
Section intitulée « Code final »#ifndef WHATEVER_HPP#define WHATEVER_HPP
template <typename T>void swap(T& a, T& b) { T temp = a; a = b; b = temp;}
template <typename T>T const& min(T const& a, T const& b) { return (a < b) ? a : b;}
template <typename T>T const& max(T const& a, T const& b) { return (a > b) ? a : b;}
#endifExercice 01 : Iter
Section intitulée « Exercice 01 : Iter »Analyse du sujet
Section intitulée « Analyse du sujet »Implémenter un template de fonction iter qui :
- Prend une adresse de tableau
- Prend la longueur du tableau
- Prend une fonction à appliquer à chaque élément
- Appelle la fonction sur chaque élément
Idée clé : C’est un algorithme générique - il fonctionne avec n’importe quel type de tableau et n’importe quelle fonction.
Stratégie d’approche
Section intitulée « Stratégie d’approche »Réfléchir avant de coder :
-
Quels sont les paramètres ?
T* array- pointeur vers le premier élémentsize_t length- nombre d’élémentsvoid (*func)(T&)- pointeur de fonction qui prend une référence d’élément
-
Avons-nous besoin d’une version const ?
- Oui ! Pour les opérations en lecture seule (comme l’affichage)
void (*func)(T const&)pour l’accès const
-
Comment appeler iter avec une fonction template ?
- Doit spécifier explicitement le type :
iter(arr, 5, print<int>) - Le compilateur ne peut pas déduire le paramètre template d’un pointeur de fonction
- Doit spécifier explicitement le type :
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 : iter de base (modification)
#ifndef ITER_HPP#define ITER_HPP
#include <cstddef> // size_t
template <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}
#endifÉtape 2 : Ajouter version const (lecture)
template <typename T>void iter(T* array, size_t length, void (*func)(T const&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Étape 3 : Fonctions de test
#include <iostream>#include "iter.hpp"
// Fonction print (const - ne modifie pas)template <typename T>void print(T const& elem) { std::cout << elem << " ";}
// Fonction double (modifie)template <typename T>void doubleValue(T& elem) { elem *= 2;}
int main() { int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original: "; iter(arr, 5, print<int>); std::cout << std::endl;
iter(arr, 5, doubleValue<int>);
std::cout << "Doubled: "; iter(arr, 5, print<int>); std::cout << std::endl;
return 0;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »Paramètre pointeur de fonction :
| Syntaxe | Signification |
|---|---|
void (*func)(T&) | Pointeur vers fonction prenant T& et retournant void |
void (*func)(T const&) | Pointeur vers fonction prenant T const& et retournant void |
func(array[i]) | Appeler la fonction avec l’élément comme argument |
Pourquoi deux surcharges ?
// Version modificatrice - quand la fonction change les élémentsvoid increment(int& n) { n++; }iter(arr, 5, increment); // Utilise void (*func)(T&)
// Version const - quand la fonction ne fait que lirevoid print(int const& n) { std::cout << n; }iter(arr, 5, print); // Utilise void (*func)(T const&)Fonction template comme argument :
// Fonction templatetemplate <typename T>void print(T const& elem) { std::cout << elem; }
// FAUX : Ne peut pas déduire Titer(arr, 5, print); // Erreur !
// CORRECT : Type expliciteiter(arr, 5, print<int>); // OKPièges courants
Section intitulée « Pièges courants »1. Surcharge const manquante :
// FAUX : Seulement version non-consttemplate <typename T>void iter(T* array, size_t length, void (*func)(T&)) { ... }
// Ceci échoue :void print(int const& n) { std::cout << n; }iter(arr, 5, print); // Erreur ! Ne peut pas convertir les types de fonction
// CORRECT : Ajouter surcharge consttemplate <typename T>void iter(T* array, size_t length, void (*func)(T const&)) { ... }2. Oublier la spécification de type :
template <typename T>void print(T const& elem) { std::cout << elem; }
// FAUX : Le compilateur ne peut pas déduireiter(arr, 5, print);
// CORRECT : Spécifier le typeiter(arr, 5, print<int>);3. Utiliser le mauvais type de taille :
// FAUX : int pour la taille (peut être négatif)void iter(T* array, int length, ...) { ... }
// CORRECT : size_t (non signé, bon type pour les tailles)void iter(T* array, size_t length, ...) { ... }Conseils de test
Section intitulée « Conseils de test »#include <iostream>#include <string>#include "iter.hpp"
template <typename T>void print(T const& elem) { std::cout << "[" << elem << "] ";}
template <typename T>void increment(T& elem) { elem++;}
int main() { // Tester avec tableau int int numbers[] = {1, 2, 3, 4, 5}; std::cout << "Int array: "; iter(numbers, 5, print<int>); std::cout << std::endl;
iter(numbers, 5, increment<int>); std::cout << "After increment: "; iter(numbers, 5, print<int>); std::cout << std::endl;
// Tester avec tableau string std::string words[] = {"hello", "world", "test"}; std::cout << "String array: "; iter(words, 3, print<std::string>); std::cout << std::endl;
// Tester avec tableau vide int empty[1] = {0}; iter(empty, 0, print<int>); // Ne devrait rien faire std::cout << "Empty test passed" << std::endl;
return 0;}Script de test bash :
c++ -Wall -Wextra -Werror -std=c++98 main.cpp -o iter./iterCode final
Section intitulée « Code final »#ifndef ITER_HPP#define ITER_HPP
#include <cstddef>
template <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}
template <typename T>void iter(T* array, size_t length, void (*func)(T const&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}
#endif#include <iostream>#include <string>#include "iter.hpp"
template <typename T>void print(T const& elem) { std::cout << elem << " ";}
template <typename T>void doubleValue(T& elem) { elem *= 2;}
int main() { int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original: "; iter(arr, 5, print<int>); std::cout << std::endl;
iter(arr, 5, doubleValue<int>);
std::cout << "Doubled: "; iter(arr, 5, print<int>); std::cout << std::endl;
std::string strs[] = {"Hello", "World"}; std::cout << "Strings: "; iter(strs, 2, print<std::string>); std::cout << std::endl;
return 0;}Exercice 02 : Array
Section intitulée « Exercice 02 : Array »Analyse du sujet
Section intitulée « Analyse du sujet »Implémenter un template de classe Array<T> qui :
- Stocke des éléments de n’importe quel type T
- Supporte la construction par défaut (tableau vide)
- Supporte la construction avec taille avec éléments initialisés par défaut
- OCF complet (constructeur de copie, assignation, destructeur)
operator[]avec vérification des limites (lance une exception)- Fonction membre
size()
Contraintes clés :
- Mémoire allouée avec
new[] - Doit utiliser des exceptions pour hors limites
- Copie profonde requise (les changements sur la copie n’affectent pas l’original)
Stratégie d’approche
Section intitulée « Stratégie d’approche »Réfléchir avant de coder :
-
Quels membres avons-nous besoin ?
T* _array- tableau alloué dynamiquementunsigned int _size- nombre d’éléments
-
Pourquoi copie profonde ?
- Chaque Array possède sa propre mémoire
- Copie superficielle = deux Arrays pointant vers la même mémoire = désastre
-
Comment initialiser les éléments par défaut ?
new T[n]()- les()déclenchent l’initialisation par valeur- Les entiers deviennent 0, les pointeurs deviennent NULL, les objets utilisent le constructeur par défaut
-
Implémentation dans le header seulement - pourquoi ?
- Les templates nécessitent une implémentation visible au point d’instanciation
- Le compilateur génère du code quand vous utilisez
Array<int>,Array<std::string>, etc. - Ne peut pas être dans un fichier .cpp (l’éditeur de liens ne le trouvera pas)
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 : Squelette de classe avec membres
#ifndef ARRAY_HPP#define ARRAY_HPP
#include <stdexcept> // std::out_of_range
template <typename T>class Array {private: T* _array; unsigned int _size;
public: // Constructeurs & Destructeur Array(); Array(unsigned int n); Array(const Array& other); ~Array();
// Assignation Array& operator=(const Array& other);
// Accès aux éléments T& operator[](unsigned int index); const T& operator[](unsigned int index) const;
// Getter de taille unsigned int size() const;};
#endifÉtape 2 : Constructeurs par défaut et de taille
template <typename T>Array<T>::Array() : _array(NULL), _size(0) {}
template <typename T>Array<T>::Array(unsigned int n) : _array(new T[n]()), _size(n) {}Étape 3 : Constructeur de copie et destructeur
template <typename T>Array<T>::Array(const Array& other) : _array(NULL), _size(0) { *this = other; // Utiliser l'opérateur d'assignation}
template <typename T>Array<T>::~Array() { delete[] _array;}Étape 4 : Opérateur d’assignation (copie profonde)
template <typename T>Array<T>& Array<T>::operator=(const Array& other) { if (this != &other) { delete[] _array; // Libérer ancienne mémoire _size = other._size; _array = new T[_size]; for (unsigned int i = 0; i < _size; i++) { _array[i] = other._array[i]; } } return *this;}Étape 5 : Accès aux éléments avec vérification des limites
template <typename T>T& Array<T>::operator[](unsigned int index) { if (index >= _size) { throw std::out_of_range("Array index out of bounds"); } return _array[index];}
template <typename T>const T& Array<T>::operator[](unsigned int index) const { if (index >= _size) { throw std::out_of_range("Array index out of bounds"); } return _array[index];}
template <typename T>unsigned int Array<T>::size() const { return _size;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »Initialisation par valeur :
| Syntaxe | Effet |
|---|---|
new T[n] | Initialisation par défaut (valeurs indéterminées pour primitifs) |
new T[n]() | Initialisation par valeur (0 pour primitifs) |
// Sans () - contient des valeurs indéterminéesint* arr1 = new int[5]; // arr1[0] = ??? (indéfini)
// Avec () - initialisé à zéroint* arr2 = new int[5](); // arr2[0] = 0Copie profonde dans l’assignation :
template <typename T>Array<T>& Array<T>::operator=(const Array& other) { if (this != &other) { // Vérification auto-assignation delete[] _array; // Libérer mémoire existante _size = other._size; // Copier taille _array = new T[_size]; // Allouer nouvelle mémoire for (unsigned int i = 0; i < _size; i++) { _array[i] = other._array[i]; // Copier chaque élément } } return *this;}Pourquoi deux surcharges operator[] ?
// Non-const : pour modifier les élémentsT& operator[](unsigned int index);arr[0] = 42; // Utilise non-const
// Const : pour lire depuis un Array constconst T& operator[](unsigned int index) const;const Array<int> arr(5);int x = arr[0]; // Utilise version constPièges courants
Section intitulée « Pièges courants »1. Copie superficielle :
// FAUX : Les deux tableaux pointent vers la même mémoireArray(const Array& other) { _size = other._size; _array = other._array; // Copie superficielle !}// Quand l'un est détruit, l'autre a un pointeur invalide
// CORRECT : Allouer nouvelle mémoire et copier élémentsArray(const Array& other) : _array(NULL), _size(0) { *this = other; // Utilise assignation copie profonde}2. Initialisation par valeur manquante :
// FAUX : Valeurs indéterminées pour primitifsArray(unsigned int n) : _array(new T[n]), _size(n) {}
// CORRECT : Initialisé par valeur (zéros pour int, etc.)Array(unsigned int n) : _array(new T[n]()), _size(n) {}3. Ne pas vérifier l’auto-assignation :
// FAUX : Supprime ses propres données avant de copierArray& operator=(const Array& other) { delete[] _array; // Oups, supprimé nos propres données ! _array = new T[other._size]; // ... copier depuis other (qui est maintenant des déchets)}
// CORRECT : Vérifier d'abordArray& operator=(const Array& other) { if (this != &other) { delete[] _array; // ... sûr de copier } return *this;}4. Utiliser int au lieu de unsigned int :
// FAUX : Comparaison signée peut être problématiqueif (index >= _size) // Avertissement : comparaison signé et non-signé
// Le sujet spécifie unsigned intT& operator[](unsigned int index);5. Implémentation dans fichier .cpp :
// FAUX : Erreur de l'éditeur de lienstemplate <typename T>Array<T>::Array() { ... }
// CORRECT : L'implémentation doit être dans le header// Array.hpp (ou Array.tpp inclus par .hpp)template <typename T>Array<T>::Array() { ... }Conseils de test
Section intitulée « Conseils de test »#include <iostream>#include "Array.hpp"
int main() { // Tester constructeur par défaut Array<int> empty; std::cout << "Empty size: " << empty.size() << std::endl;
// Tester constructeur de taille Array<int> arr(5); std::cout << "Size 5 array: " << arr.size() << std::endl;
// Tester initialisation par valeur std::cout << "Initial values: "; for (unsigned int i = 0; i < arr.size(); i++) { std::cout << arr[i] << " "; } std::cout << std::endl;
// Tester modification for (unsigned int i = 0; i < arr.size(); i++) { arr[i] = i * 10; } std::cout << "After modification: "; for (unsigned int i = 0; i < arr.size(); i++) { std::cout << arr[i] << " "; } std::cout << std::endl;
// Tester copie profonde Array<int> copy = arr; copy[0] = 999; std::cout << "Original[0]: " << arr[0] << std::endl; std::cout << "Copy[0]: " << copy[0] << std::endl;
// Tester vérification des limites try { arr[100] = 42; } catch (std::exception& e) { std::cout << "Exception: " << e.what() << std::endl; }
// Tester avec type différent Array<std::string> strings(3); strings[0] = "Hello"; strings[1] = "World"; strings[2] = "!"; for (unsigned int i = 0; i < strings.size(); i++) { std::cout << strings[i] << " "; } std::cout << std::endl;
return 0;}Script de test bash :
c++ -Wall -Wextra -Werror -std=c++98 main.cpp -o array_test./array_test
# Sortie attendue :# Empty size: 0# Size 5 array: 5# Initial values: 0 0 0 0 0# After modification: 0 10 20 30 40# Original[0]: 0# Copy[0]: 999# Exception: Array index out of bounds# Hello World !Code final
Section intitulée « Code final »#ifndef ARRAY_HPP#define ARRAY_HPP
#include <stdexcept>
template <typename T>class Array {private: T* _array; unsigned int _size;
public: Array(); Array(unsigned int n); Array(const Array& other); ~Array();
Array& operator=(const Array& other);
T& operator[](unsigned int index); const T& operator[](unsigned int index) const;
unsigned int size() const;};
// Constructeur par défauttemplate <typename T>Array<T>::Array() : _array(NULL), _size(0) {}
// Constructeur de taille avec initialisation par valeurtemplate <typename T>Array<T>::Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Constructeur de copietemplate <typename T>Array<T>::Array(const Array& other) : _array(NULL), _size(0) { *this = other;}
// Destructeurtemplate <typename T>Array<T>::~Array() { delete[] _array;}
// Opérateur d'assignation (copie profonde)template <typename T>Array<T>& Array<T>::operator=(const Array& other) { if (this != &other) { delete[] _array; _size = other._size; _array = new T[_size]; for (unsigned int i = 0; i < _size; i++) { _array[i] = other._array[i]; } } return *this;}
// Accès aux éléments (non-const)template <typename T>T& Array<T>::operator[](unsigned int index) { if (index >= _size) { throw std::out_of_range("Array index out of bounds"); } return _array[index];}
// Accès aux éléments (const)template <typename T>const T& Array<T>::operator[](unsigned int index) const { if (index >= _size) { throw std::out_of_range("Array index out of bounds"); } return _array[index];}
// Getter de tailletemplate <typename T>unsigned int Array<T>::size() const { return _size;}
#endif#include <iostream>#include <string>#include "Array.hpp"
int main() { // Tester tableau vide Array<int> empty; std::cout << "Empty size: " << empty.size() << std::endl;
// Tester constructeur de taille Array<int> arr(5); std::cout << "Values initialized to: "; for (unsigned int i = 0; i < arr.size(); i++) std::cout << arr[i] << " "; std::cout << std::endl;
// Tester modification for (unsigned int i = 0; i < arr.size(); i++) arr[i] = i * 10;
// Tester copie profonde Array<int> copy(arr); copy[0] = 999; std::cout << "Original[0]=" << arr[0] << ", Copy[0]=" << copy[0] << std::endl;
// Tester exception try { std::cout << arr[100] << std::endl; } catch (std::exception& e) { std::cout << "Exception caught: " << e.what() << std::endl; }
return 0;}Résumé du Module 07 : Templates
Section intitulée « Résumé du Module 07 : Templates »| Concept | Point clé |
|---|---|
| Template de fonction | template <typename T> avant la fonction |
| Template de classe | template <typename T> avant la classe |
| Instanciation | Le compilateur génère du code pour chaque type utilisé |
| Header seulement | Les templates doivent être définis où ils sont utilisés |
| Exigences de type | T doit supporter les opérations utilisées dessus |
Référence syntaxe template :
// Template de fonctiontemplate <typename T>T const& min(T const& a, T const& b);
// Template de classetemplate <typename T>class Array { T* data;public: Array(); T& operator[](unsigned int i);};
// Définition de méthode hors classetemplate <typename T>Array<T>::Array() : data(NULL) {}
// Utiliser les templatesArray<int> intArray(10);Array<std::string> strArray(5);Référence rapide
Section intitulée « Référence rapide »Template de fonction
Section intitulée « Template de fonction »template <typename T>T functionName(T param) { /* ... */ }Template de classe
Section intitulée « Template de classe »template <typename T>class ClassName { // Membres utilisant T};
// Définition hors classetemplate <typename T>ClassName<T>::methodName() { /* ... */ }Règles clés
Section intitulée « Règles clés »- Les templates doivent être dans les headers (ou .tpp inclus)
- Utiliser
typenameouclass(interchangeables) - Les types doivent supporter les opérations utilisées dans le template
- Copie profonde pour les membres pointeurs dans les templates de classes
Concepts connexes
Section intitulée « Concepts connexes »Continuez votre parcours C++ :
- Précédent : Module 06 : Casts C++ - Réviser les conversions de types
- Suivant : Module 08 : Conteneurs STL - Apprendre les conteneurs construits avec des templates
Termes clés de ce module
Section intitulée « Termes clés de ce module »Visitez le Glossaire pour les définitions de :