Introduction aux paramètres de template
Dans notre précédente exploration des templates en C++, nous avons examiné les bases des paramètres de template. Maintenant, plongeons plus profondément dans leurs fonctionnalités avancées et leurs applications spécifiques.
1. Paramètres de template non-typés
Les paramètres de template se divisent en deux catégories : les paramètres de type et les paramètres non-typés.
Qu'est-ce qu'un paramètre non-typé ? Il s'agit d'utiliser une contsante comme paramètre pour une classe ou un template de fonction. Dans le template, ce paramètre peut être traité comme une cosntante.
// Exemple de paramètre de template non-typé
namespace avances
{
template<typename T, size_t Capacite = 20>
class Pile
{
public:
Pile()
:sommet(0)
,donnees(new T[Capacite]{})
{}
bool vide()
{
return sommet == 0;
}
private:
T* donnees;
int sommet;
};
}
Cette approche peut sembler similaire aux macros. Examinons cela de plus près :
Les macro-instructions se divisent également en deux types : les macros sans paramètres et les macros avec paramètres.
Macros sans paramètres : Simple substitution de texte, au format #define NomMacro Constante / Chaîne.
#include <stdio.h>
// Macro sans paramètre : utilisation de PI pour représenter 3.1415926
#define PI 3.1415926
int main() {
// Avant compilation : r=2*PI*5 → Après compilation : r = 2*3.1415926*5
double r = 2 * PI * 5;
printf("Circonférence : %f", r);
return 0;
}
Macros avec paramètres : Similaire à une fonction, mais ce n'est pas une fonction, c'est une substitution de texte, au format #define NomMacro(paramètres) CorpsDeSubstitution.
#include <stdio.h>
// Macro avec paramètre : calcul du carré (les parenthèses sont cruciales !)
// Il faut ajouter des parenthèses à tous les paramètres et à l'ensemble pour éviter les problèmes de priorité.
#define carre(x) ((x) * (x))
int main() {
// Avant compilation : carre(3+1) → Après compilation : ((3+1)*(3+1))=16
printf("%d", carre(3+1));
return 0;
}
Quelle est la différence entre les paramètres de template non-typés et les macros ?
La différence réside dans la flexibilité : les macros écrivent le programme de manière figée. Par exemple, dans le programme initial, si la fonction main est appelée deux fois, les tableaux créés auront tous une taille de 10. Les paramètres de template non-typés permettent d'ajuster dynamiquement la taille du tableau.
Points importants concernant les paramètres de template non-typés :
- Les nombres à virgule flottante, les objets de classe et les chaînes de caractères ne peuvent pas être utilisés comme paramètres de template non-typés.
- Les paramètres de template non-typés doivent être connus à la compilation.
Explications :
Pourquoi les nombres à virgule flottante ne sont-ils pas autorisés ? Différents compilateurs peuvent gérer la précision des nombres à virgule flottante différemment. Pour les templates, la valeur doit être strictement comparable et déterminée de manière unique. Les nombres à virgule flottante peuvent perdre de la précision, ce qui les rend inappropriés.
Pourquoi les objets de classe ne sont-ils pas autorisés ? Il est impossible d'instancier et de comparer l'égalité à la compilation. Les objets de classe n'existent qu'à l'instanciation (c'est-à-dire au moment de l'exécution). Le type du paramètre est déterminé à la phase de compilation pour informer le compilateur de l'existence de la classe.
Pourquoi les chaînes de caractères ne sont-elles pas autorisées ? Les chaînes sont des tableaux ou des pointeurs, et des chaînes identiques définies à différents endroits peuvent avoir des adresses différentes ! Le compilateur peut générer deux adresses différentes. Les templates ne peuvent pas déterminer s'il s'agit de la même valeur.
template<const char* Texte>
void afficher() {
std::cout << Texte << std::endl;
}
int main() {
afficher<"bonjour">(); // Erreur : les littéraux de chaîne ne peuvent pas être des paramètres de template non-typés
}
2. Spécialisation des templates
2.1 Spécialisation des templates de fonction
Dans la plupart des cas, l'utilisation des templates permet d'écrire du code indépendant du type. Cependant, pour certains types spécifiques, on peut obtenir des résultats incorrects qui nécessitent un traitement spécialisé. Par exemple, implémentons une fonction template spécialisée pour les comparaisons inférieures.
template<typename T>
bool inferieur(T gauche, T droite)
{
return gauche < droite;
}
int main()
{
inferieur<int> cmp1;
std::cout << cmp1(1, 2) << std::endl;
std::cout << cmp1(10, 2) << std::endl; // correct
inferieur<Date> cmp2;
Date d1(2025, 5, 1);
Date d2(2025, 9, 10);
std::cout << cmp2(d1, d2) << std::endl; // correct
inferieur<Date*> cmp3;
std::cout << cmp3(&d1, &d2) << std::endl;
std::cout << cmp3(new Date(2025, 10, 1), new Date(2025, 9, 1)) << std::endl; //erreur
return 0;
}
Les deux premiers appels de la fonction inferieur sont corrects, mais le dernier est incorrect car il compare des adresses. Cela a été expliqué en détail dans l'introduction des foncteurs. La solution à l'époque consistait à réécrire un foncteur spécialisé pour la comparaison d'adresses, ce qui est très similaire à la spécialisation de template que nous allons aborder ici.
Étapes de la spécialisation de template :
- Il doit d'abord y avoir un template de fonction de base.
- Le mot-clé
templateest suivi d'une paire de crochets vides<>.- Le nom de la fonction est suivi d'une paire de crochets spécifiant le type à spécialiser.
- La liste des paramètres formels : doit être identique aux types de paramètres du template de base, sinon le compilateur peut générer des erreurs étranges.
// Template de fonction template<typename T> bool inferieur(const T& gauche, const T& droite) { return gauche < droite; } // Spécialisation du template de fonction template<> bool inferieur<Date*>(const Date* gauche, const Date* droite) { return *gauche < *droite; }Concernant le quatrième point : (les types des deux côtés doivent être cohérents)
En pratique, les programmeurs utilisent souvent une approche plus directe :
// Méthode courante bool inferieur(const Date* gauche, const Date* droite) { return *gauche < *droite; }Comme on dit : pourquoi réinventer la roue ? Si le type correspond exactement, le compilateur privilégiera également l'appel de la fonction la plus spécifique.
2.2 Spécialisation des templates de classe
2.2.1 Spécialisation complète
La spécialisation complète consiste à déterminer tous les paramètres de la liste de paramètres de template.
template
class Conteneur
{
public:
Conteneur()
{
std::cout << "Conteneur" << std::endl;
}
private:
T1 tempsEtude;
T2 tachesMaison;
};
// Spécialisation complète
template<>
class Conteneur
{
public:
Conteneur()
{
std::cout << "Conteneur" << std::endl;
}
private:
int tempsEtude;
double tachesMaison;
};
int main()
{
Conteneur c1; // appelle le premier
Conteneur c2; // appelle le deuxième
Conteneur c3;// appelle le premier
Conteneur c4; // appelle le premier
Conteneur c5; // appelle le premier
return 0;
}
Résultat d'exécution :
Conteneur
Conteneur
Conteneur
Conteneur
Conteneur
On constate que dans la spécialisation complète, T1 et T2 sont explicitement définis.
Point d'erreur courant : Certains étudiants ont tenté de spécialiser complètement les foncteurs précédents mais ont constaté des erreurs, bien qu'ils aient suivi les instructions correctement.
// Foncteur
template
class Inferieur
{
public:
bool operator()(const T1& gauche, const T2& droite)
{
return gauche < droite;
}
};
// Spécialisation complète
template<>
class Inferieur
{
public:
bool operator()(Date* & gauche, Date* & droite)
{
return *gauche < *droite;
}
};
Il faut que les modificateurs d'objet soient cohérents, c'est-à-dire qu'ils modifient tous les deux gauche et droite directement. Le code correct pour la spécialisation complète est :
// Spécialisation complète
template<>
class Inferieur
{
public:
bool operator()(Date* const & gauche, Date* const & droite)
{
return *gauche < *droite;
}
};
2.2.2 Spécialisation partielle
La spécialisation partielle est toute version spécialisée qui impose des restrictions supplémentaires sur les paramètres de template.
La spécialisation partielle se divise en deux types : (1) spécialisation partielle des paramètres ; (2) restrictions supplémentaires sur les paramètres dans le cadre de la spécialisation partielle.
(1) Spécialisation partielle des paramètres :
// Spécialisation partielle 1
template// Paramètre T1 inchangé
class Conteneur
{
public:
Conteneur()
{
std::cout << "Conteneur" << std::endl;
}
private:
T1 tempsEtude;
double tachesMaison;
};
Ici, T2 est explicitement défini comme double, et on impose une restriction supplémentaire sur les paramètres de la classe, passant d'une approche générique à une implémentation spécialisée.
(2) Restrictions supplémentaires sur la base de la spécialisation partielle.
La spécialisation partielle ne signifie pas seulement spécialiser certains paramètres, mais plutôt concevoir une version spécialisée qui impose des conditions supplémentaires sur les paramètres de template.
// Spécialisation partielle 2
// Pour utiliser des noms de type dépendant des paramètres de template (c'est-à-dire que l'existence de ce type dépend du paramètre de template), il faut utiliser typename pour le qualifier.
template
class Conteneur
{
public:
Conteneur()
{
std::cout << "Conteneur" << std::endl;
}
private:
T1 tempsEtude;
double tachesMaison;
};
template
class Conteneur
{
public:
Conteneur()
{
std::cout << "Conteneur" << std::endl;
}
private:
T1 tempsEtude;
double tachesMaison;
};
int main()
{
Donnees d3; // appelle le premier
Donnees d4(1, 2); // appelle le deuxième
return 0;
}
Ici, l'utilisation de class est également possible. Alors, quand typename et class sont-ils différents ?
2.2.3 Cas d'utilisation de typename
Analyse des cas d'utilisation de
typename:Il faut d'abord comprendre la compilation des templates (en C++, la compilation des templates est en deux phases) :
Première phase : lors de l'écriture du template, le compilateur ne sait pas quel est le type T, il ne vérifie que la syntaxe
Deuxième phase : lors de l'appel du template, T est remplacé par un type spécifique (int, string, pointeur, etc.)
Point crucial :
(1) Lorsque le compilateur voit
T::, il suppose par défaut qu'il s'agit d'une variable membre statique ou d'une fonction statique, jamais d'un type !(2) Si
T::est réellement un type imbriqué (par exemple, un alias défini dans une classe, une sous-classe), il faut utilisertypenamepour le signaler au compilateur.#include<vector> void Afficher(const vector<int>& v) { vector<int>::const_iterator it = v.begin(); while (it != v.end()) { std::cout << *it << " "; it++; } std::cout << std::endl; } template<typename T> void Afficher(const vector<T>& v) { typename vector<T>::const_iterator it = v.begin(); while (it != v.end()) { std::cout << *it << " "; it++; } std::cout << std::endl; }Dans ce cas, la deuxième écriture doit inclure
typename. Si on le supprime avantvector, le compilateur génère une erreur.Explication :
Tout type imbriqué dépendant d'un paramètre de template doit être précédé de
typename!!!
3. Séparation et compilation des templates
Créons trois fichiers :
main.cpp
#include "fonctions.h"
int main()
{
std::cout << additionner(1, 2) << std::endl;
//std::cout << additionner(1.0, 5.6) << std::endl;
}
fonctions.h
#pragma once
int additionner(int gauche, int droite);
template
int additionner(T gauche, T droite);
fonctions.cpp
#include "fonctions.h"
int additionner(int gauche, int droite)
{
return gauche + droite;
}
Pour comprendre la séparation et la compilation des templates, il faut connaître le processus :
Analyse détaillée du processus :
Phase de prétraitement : le fichier fonctions.h est développé dans main.cpp, les commentaires sont supprimés et les macros sont remplacées, ce qui donne le fichier main.i (vous pouvez vérifier dans vos fichiers, il existe certainement)
Phase de compilation : le compilateur traduit le programme en langage d'assemblage et génère les adresses correspondantes des fonctions, les plaçant dans une table des symboles. Cependant, à ce stade, le compilateur ne peut pas obtenir ces adresses car la compilation des fichiers est unidirectionnelle. Le compilateur remplit donc d'abord une adresse d'espace réservé à la position de l'instruction call, générant le fichier main.s
Phase d'assemblage : le langage d'assemblage est converti en instructions compréhensibles par la machine (fichier binaire)
Phase d'édition de liens : les fichiers sont fusionnés et un exécutable est généré. À ce stade, le compilateur peut trouver les adresses des fonctions dans la table des symboles et les remplacer (c'est-à-dire remplacer l'adresse d'espace réservée par l'adresse relative de fonctions.cpp).
Phase de chargement du programme : le système convertit l'adresse relative en adresse d'entrée réelle de fonctions.cpp dans le processus actuel !!
Question : À la phase d'édition de liens, le compilateur recherche-t-il les adresses de fonction dans tous les fichiers ? Ne serait-ce pas inefficace s'il y avait de nombreux fichiers ?
NONONO !!! Les adresses des fonctions sont placées dans une table des symboles à la phase d'assemblage (génération des instructions machine), et le compilateur les recherche dans cette table.
Il est déconseillé de séparer les templates, pourquoi ?
Pourquoi les templates ne peuvent-ils pas être séparés ? Cela est dû au fait qu'un template nécessite encore une instanciation pour être compilé, c'est-à-dire que T ne peut pas être connu !
Qui connaît T alors ? - main.cpp connaît T ! Mais comme l'opération est unidirectionnelle, les deux fichiers ne peuvent pas interagir, donc on ne sait pas quel est T, on ne peut donc pas instancier, et donc l'adresse de la fonction est inconnue.
Cependant, le fichier d'en-tête du template dans fonctions.cpp indique au compilateur de ne pas s'inquiéter, il lui transmettra le résultat plus tard. Mais lorsque le compilateur demande l'adresse, il ne l'a pas. N'est-ce pas une tromperie ?
Les templates peuvent-ils vraiment être séparés et compilés ? Oui, mais c'est fortement déconseillé.
L'instanciation explicite vise à informer le compilateur que cette fois la promesse est tenue, et que son type est int.
fonctions.cpp
#pragma once
#include "fonctions.h"
int additionner(int gauche, int droite)
{
return gauche + droite;
}
template
int additionner(T gauche, T droite)
{
return gauche + droite;
}
// Déclaration d'instanciation explicite du template
template
int additionner(int gauche, int droite);
fonctions.h
#pragma once
int additionner(int gauche, int droite);
template
int additionner(T gauche, T droite);
Ceci conclut notre exploration des templates en C++. Si ce contenu vous a été utile, n'hésitez pas à le liker et à le sauvegarder !