Programmation • IoT

Nous rencontrons souvent le même algorithme appliqué à plusieurs types. Une fonction qui cherche le maximum de deux int et une autre qui compare deux double réalisent exactement le même travail ; seul le type change. Multiplier les surcharges recopierait inutilement le code. Les templates permettent donc de séparer l’algorithme du type des données qu’il manipule.

Un template de fonction est introduit par template <typename T>. Le symbole T n’est pas un type universel ni une valeur : c’est un paramètre qui représentera un type précis lors de la compilation. Dans T maximum(T a, T b), les deux paramètres et le résultat utilisent le même type, encore inconnu au moment où le modèle est écrit.

Lors d’un appel comme maximum(5, 8), le compilateur déduit T = int et instancie la fonction correspondante. Un appel avec deux double produit une autre instanciation. Le programmeur écrit donc un seul modèle, mais le compilateur génère uniquement les versions concrètes nécessaires aux types réellement employés.

La déduction repose sur les arguments de la fonction, et non sur le type de la variable qui recevra le résultat. L’appel maximum(5, 8.7) échoue avec un unique paramètre T, car le premier argument suggère int et le second double. On peut résoudre cette ambiguïté en convertissant un argument ou en imposant explicitement maximum<double>(5, 8.7).

Plusieurs paramètres de type, par exemple template <typename T, typename U>, autorisent des arguments de types différents. Cette généralisation pose alors la question du type du résultat. Le déclarer arbitrairement comme T ou U peut perdre de l’information ; auto permet au compilateur de le déduire à partir de l’expression retournée. Accepter plusieurs types doit toutefois correspondre au contrat voulu, et non à une recherche systématique de généralité.

Un template ne fonctionne pas magiquement avec n’importe quel type. Le code qu’il contient impose des exigences : a > b demande une comparaison valide, tandis que a + b exige une addition. Une classe créée par le programmeur peut satisfaire ces exigences grâce à ses opérateurs surchargés. La généricité dépend donc moins du nom des types que des opérations qu’ils savent effectuer.

Le même principe s’applique aux classes. Une classe Boite limitée à int peut devenir Boite<T>, et le paramètre T détermine alors le type de son attribut, des paramètres de son constructeur et des valeurs retournées. Boite<int>, Boite<double> et Boite<Complexe> forment une famille de classes, mais constituent des types C++ distincts.

Le paramètre de type n’est pas stocké comme une donnée supplémentaire dans chaque objet : il sert au compilateur à former le type concret. Plusieurs instanciations peuvent néanmoins augmenter la taille du programme, puisque différentes versions des fonctions sont générées. Cette distinction entre la mémoire occupée par les objets et celle occupée par le code est particulièrement importante sur un système embarqué.

Les classes templates imposent une organisation particulière des fichiers. Pour instancier Boite<int>, le compilateur doit généralement voir non seulement la déclaration de la classe, mais aussi la définition de ses fonctions. Placer celles-ci dans un fichier .cpp compilé séparément ne les rend donc pas automatiquement disponibles au point d’instanciation.

On peut tout placer dans le fichier d’en-tête ou conserver une séparation lisible avec un fichier .tpp. Le fichier Boite.h déclare la classe puis inclut Boite.tpp à sa fin, afin que le compilateur voie l’ensemble du template. Le fichier .tpp n’est pas une unité de compilation autonome : son contenu est absorbé par le fichier d’en-tête, comme un fichier LaTeX inclus avec \input.

Les templates évitent ainsi de dupliquer un même raisonnement pour chaque type tout en préservant le typage strict du C++. Leur puissance ne vient pas d’un effacement des types, mais de la capacité du compilateur à produire des fonctions et des classes précisément adaptées. Écrire du code générique consiste donc à identifier ce qui dépend du type, mais aussi à rendre visibles les opérations que chaque type devra fournir.