Aller au contenu

Module 07 : Templates C++

Télécharger le PDF officiel du sujet

Concepts clés :

  • Templates de fonctions
  • Templates de classes
  • Instanciation de templates
  • Spécialisation de templates

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.


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); // Fonctionne
double d = MAX(3.14, 2.71); // Fonctionne

Mais les macros ont de sérieux problèmes :

  1. Pas de sécurité de type : MAX("hello", 42) compile mais n’a aucun sens
  2. Bugs de substitution de texte : MAX(i++, j) incrémente i deux fois si i > j
  3. Pas de portée : Les macros polluent l’espace de noms global
  4. 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)

Pensez aux templates comme des patrons de code avec des trous (placeholders) que le compilateur remplit :

template <typename T> // "T" est le trou
T 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.


Ce module introduit les templates. Voici la nouvelle syntaxe expliquée :

template <typename T>
T max(T a, T b) { return (a > b) ? a : b; }
  • template <> déclare que ce qui suit est un template
  • typename T (ou class T) déclare un paramètre de type appelé T
  • T peut ê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
  • typename et class sont interchangeables en C++98 (les deux signifient la même chose)
max<int>(3, 5); // Utiliser explicitement int
max(3.14, 2.71); // Le compilateur déduit double
Array<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 »
std::vector<int>::iterator it;
  • :: avec les templates accède aux types imbriqués
  • vector<int>::iterator signifie « 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)
typename std::vector<T>::iterator it; // Dire au compilateur que c'est un type
  • typename avant ClassName::NestedType dit au compilateur « ceci est un type, pas une valeur »
  • À l’intérieur des templates, le compilateur ne sait pas si vector<T>::iterator est un type ou un membre statique
  • typename supprime 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
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
  • T est le type de retour, U est le type d’entrée
  • Usage : convert<int>(3.14) spécifie explicitement T, le compilateur déduit U
template <typename T = int>
class Stack { };
Stack<> intStack; // Utilise le défaut : int
Stack<double> dblStack; // Explicite : double
  • = int fournit 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 juste Stack)

// Sans templates - doit écrire pour chaque type
int 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; }
// Avec templates - une définition, fonctionne pour tous les types
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// Usage
max(3, 5); // Instancie max<int>
max(3.14, 2.71); // Instancie max<double>
max(str1, str2); // Instancie max<std::string>

template <typename T>
T functionName(T param1, T param2) {
// T peut être utilisé comme n'importe quel type
return param1 + param2;
}
template <typename T, typename U>
T convert(U value) {
return static_cast<T>(value);
}
// Usage
int i = convert<int>(3.14); // T explicite, U déduit
template <typename T> // Style moderne
template <class T> // Aussi valide, signifie la même chose

// swap : Échanger les valeurs de deux variables
template <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;
}
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;
}

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 foncteur
template <typename T, typename F>
void iter(T* array, size_t length, F func) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
// Plus explicite : F est spécifiquement un pointeur de fonction
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}

Le même template peut être surchargé pour les opérations const et non-const :

// Version non-const - peut modifier les éléments
template <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 seule
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]);
}
}
// 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-const
iter(arr, 3, increment<int>); // arr est maintenant {2, 3, 4}
// Appelle la version const
iter(arr, 3, print<int>); // Affiche les éléments
// En appelant avec une fonction template, vous devez l'instancier :
iter(arr, 3, print<int>); // Argument de template explicite
iter(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 <>
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;
}

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;
};
// 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];
}

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;
}
};
#include <stdexcept> // pour std::out_of_range
// out_of_range est utilisée pour les erreurs d'index/limites
T& 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;
}
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érales
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;
}

// 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>
// Forcer la génération de types spécifiques (rarement nécessaire)
template class Array<int>;
template void swap<double>(double&, double&);

Array.hpp
template <typename T>
class Array {
// Déclaration ET implémentation ici
};
Array.hpp
template <typename T>
class Array {
// Déclaration seulement
};
#include "Array.tpp" // Inclure l'implémentation à la fin
// Array.tpp
template <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.

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.


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.

// Template générique
template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
// Spécialisation complète pour bool
template <>
void print<bool>(bool value) {
std::cout << (value ? "true" : "false") << std::endl;
}
// Usage
print(42); // Utilise générique : affiche "42"
print(3.14); // Utilise générique : affiche "3.14"
print(true); // Utilise spécialisation : affiche "true"
// Template générique
template <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(); }
};

La spécialisation partielle fournit un template pour un sous-ensemble de types (ex : tous les pointeurs) :

// Template générique
template <typename T>
class Container {
public:
void info() { std::cout << "Generic container" << std::endl; }
};
// Spécialisation partielle pour tous les types pointeurs
template <typename T>
class Container<T*> {
public:
void info() { std::cout << "Pointer container" << std::endl; }
};
// Usage
Container<int> c1; // Utilise générique
Container<int*> c2; // Utilise spécialisation pointeur
Container<double*> c3; // Utilise spécialisation pointeur

Note : 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.

  • 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_pointer ci-dessus)

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.

  1. Lire de bas en haut : L’erreur réelle est souvent vers la fin ; les messages précédents montrent le contexte d’instanciation

  2. Chercher « instantiated from » : Cela vous dit OÙ le template a été utilisé, vous aidant à tracer le problème

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

  4. Vérifier les opérations requises : Si votre template utilise <, le type doit supporter operator<

// Ce template requiert que operator< existe pour le type T
template <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
  1. Simplifier : Commenter des parties de votre template pour isoler le problème

template <typename T>
struct is_pointer {
static const bool value = false;
};
template <typename T>
struct is_pointer<T*> {
static const bool value = true;
};
template <typename T = int>
class Stack {
// Par défaut int si aucun type spécifié
};
Stack<> intStack; // Utilise défaut : int
Stack<double> dblStack; // Explicite : double

Implémenter trois templates de fonctions :

  • swap(a, b) - échanger deux valeurs
  • min(a, b) - retourner la plus petite valeur
  • max(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;
}

Réfléchir avant de coder :

  1. 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
  2. Pourquoi utiliser ::swap au lieu de swap ?

    • :: signifie espace de noms global
    • Évite le conflit avec std::swap
    • Votre fonction doit être dans la portée globale
  3. 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
  4. Quelles opérations T doit-il supporter ?

    • swap : assignation (=)
    • min : comparaison inférieur (<)
    • max : comparaison supérieur (>)

Étape 1 : Template swap de base

whatever.hpp
#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é

whatever.hpp - min
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é

whatever.hpp - max
template <typename T>
T const& max(T const& a, T const& b) {
// Quand égal, retourner b (second argument)
return (a > b) ? a : b;
}

Décomposition de la syntaxe template :

SyntaxeSignification
template <typename T>Déclare T comme type placeholder
T& aRéférence vers T (modifiable)
T const& aRé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 arguments
T min(T a, T b) { return (a < b) ? a : b; }
// Par référence const - BON : pas de copies
T 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 b
return (a < b) ? a : b; // Retourne b quand égal
// Quand a == b, (a > b) est faux, donc retourne b
return (a > b) ? a : b; // Retourne b quand égal

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éel
template <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 égal
return (a <= b) ? a : b;
// CORRECT : Retourne le second quand égal
return (a < b) ? a : b;

3. Pas dans l’espace de noms global :

// FAUX : Dans un namespace, ::swap ne le trouvera pas
namespace MyLib {
template <typename T>
void swap(T& a, T& b) { ... }
}
// CORRECT : Dans la portée globale
template <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és
template <typename T>
void swap(T a, T b) {
T temp = a; a = b; b = temp;
}
// CORRECT : Par référence - modifie les valeurs originales
template <typename T>
void swap(T& a, T& b) {
T temp = a; a = b; b = temp;
}
main.cpp
#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=3
After swap: a=3, b=2
min: 2
max: 3
c=chaine2, d=chaine1
min: chaine1
max: chaine2
Equal test: &x=0x7fff..., &y=0x7fff...
min returns: 0x7fff... (même que &y)
max returns: 0x7fff... (même que &y)
whatever.hpp
#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;
}
#endif

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.

Réfléchir avant de coder :

  1. Quels sont les paramètres ?

    • T* array - pointeur vers le premier élément
    • size_t length - nombre d’éléments
    • void (*func)(T&) - pointeur de fonction qui prend une référence d’élément
  2. 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
  3. 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

Étape 1 : iter de base (modification)

iter.hpp
#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)

iter.hpp - surcharge const
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

main.cpp
#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;
}

Paramètre pointeur de fonction :

SyntaxeSignification
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éments
void increment(int& n) { n++; }
iter(arr, 5, increment); // Utilise void (*func)(T&)
// Version const - quand la fonction ne fait que lire
void print(int const& n) { std::cout << n; }
iter(arr, 5, print); // Utilise void (*func)(T const&)

Fonction template comme argument :

// Fonction template
template <typename T>
void print(T const& elem) { std::cout << elem; }
// FAUX : Ne peut pas déduire T
iter(arr, 5, print); // Erreur !
// CORRECT : Type explicite
iter(arr, 5, print<int>); // OK

1. Surcharge const manquante :

// FAUX : Seulement version non-const
template <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 const
template <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éduire
iter(arr, 5, print);
// CORRECT : Spécifier le type
iter(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, ...) { ... }
main.cpp
#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 :

Fenêtre de terminal
c++ -Wall -Wextra -Werror -std=c++98 main.cpp -o iter
./iter
iter.hpp
#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
main.cpp
#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;
}

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)

Réfléchir avant de coder :

  1. Quels membres avons-nous besoin ?

    • T* _array - tableau alloué dynamiquement
    • unsigned int _size - nombre d’éléments
  2. Pourquoi copie profonde ?

    • Chaque Array possède sa propre mémoire
    • Copie superficielle = deux Arrays pointant vers la même mémoire = désastre
  3. 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
  4. 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)

Étape 1 : Squelette de classe avec membres

Array.hpp
#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

Array.hpp - constructeurs
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

Array.hpp - copie & 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)

Array.hpp - assignation
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

Array.hpp - operator[]
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;
}

Initialisation par valeur :

SyntaxeEffet
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ées
int* arr1 = new int[5]; // arr1[0] = ??? (indéfini)
// Avec () - initialisé à zéro
int* arr2 = new int[5](); // arr2[0] = 0

Copie 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éments
T& operator[](unsigned int index);
arr[0] = 42; // Utilise non-const
// Const : pour lire depuis un Array const
const T& operator[](unsigned int index) const;
const Array<int> arr(5);
int x = arr[0]; // Utilise version const

1. Copie superficielle :

// FAUX : Les deux tableaux pointent vers la même mémoire
Array(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éments
Array(const Array& other) : _array(NULL), _size(0) {
*this = other; // Utilise assignation copie profonde
}

2. Initialisation par valeur manquante :

// FAUX : Valeurs indéterminées pour primitifs
Array(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 copier
Array& 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'abord
Array& 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ématique
if (index >= _size) // Avertissement : comparaison signé et non-signé
// Le sujet spécifie unsigned int
T& operator[](unsigned int index);

5. Implémentation dans fichier .cpp :

Array.cpp
// FAUX : Erreur de l'éditeur de liens
template <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() { ... }
main.cpp
#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 :

Fenêtre de terminal
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 !
Array.hpp
#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éfaut
template <typename T>
Array<T>::Array() : _array(NULL), _size(0) {}
// Constructeur de taille avec initialisation par valeur
template <typename T>
Array<T>::Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Constructeur de copie
template <typename T>
Array<T>::Array(const Array& other) : _array(NULL), _size(0) {
*this = other;
}
// Destructeur
template <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 taille
template <typename T>
unsigned int Array<T>::size() const {
return _size;
}
#endif
main.cpp
#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;
}

ConceptPoint clé
Template de fonctiontemplate <typename T> avant la fonction
Template de classetemplate <typename T> avant la classe
InstanciationLe compilateur génère du code pour chaque type utilisé
Header seulementLes templates doivent être définis où ils sont utilisés
Exigences de typeT doit supporter les opérations utilisées dessus

Référence syntaxe template :

// Template de fonction
template <typename T>
T const& min(T const& a, T const& b);
// Template de classe
template <typename T>
class Array {
T* data;
public:
Array();
T& operator[](unsigned int i);
};
// Définition de méthode hors classe
template <typename T>
Array<T>::Array() : data(NULL) {}
// Utiliser les templates
Array<int> intArray(10);
Array<std::string> strArray(5);

template <typename T>
T functionName(T param) { /* ... */ }
template <typename T>
class ClassName {
// Membres utilisant T
};
// Définition hors classe
template <typename T>
ClassName<T>::methodName() { /* ... */ }
  1. Les templates doivent être dans les headers (ou .tpp inclus)
  2. Utiliser typename ou class (interchangeables)
  3. Les types doivent supporter les opérations utilisées dans le template
  4. Copie profonde pour les membres pointeurs dans les templates de classes

Continuez votre parcours C++ :

Visitez le Glossaire pour les définitions de :