Un étudiant à moi (le brillant Vincent Thériault, cohorte 2 du Diplôme de développement du jeu vidéo à l'Université de Sherbrooke), m'a envoyé ces questions :
L'exemple de Vincent était (je résume) la classe Jambon suivante.
template <class T>
class Jambon
{
// ...
public:
// ...
void manger(T bouffe);
template <class U>
void boire(U boisson);
// ...
};
Dans cet exemple, un Jambon<T> mange du T mais peut boire plusieurs boissons de types différents.
Si nous définissions ces méthodes à même la déclaration de la classe, alors la solution serait relativement banale (il suffirait d'ajouter les accolades et le code), mais le problème ici est de définir les méthodes après la déclaration de la classe (en bonne partie pour des raisons de lisibilité dans le code; on se doute que l'ami Vincent ne code pas vraiment une classe Jambon ici et que les méthodes qui le préoccupent sont plutôt subtiles).
La solution au problème va donc comme suit. Dans le cas de la méthode manger(), l'écriture requise est :
template <class T>
void Jambon<T>::manger(T bouffe)
{
// ...
}
Dans le cas (plus subtil, on s'en doute) de la méthode boire(), l'écriture est :
template <class T>
template <class U>
void Jambon<T>::boire(U boisson)
{
// ...
}
On voit donc comment est représentée cette double généricité, soit par une méthode générique sur un type dans une classe générique sur un autre type.
Et voilà.
Pour lire cette partie de l'article, il est préférable d'avoir au préalable digéré quelques techniques de métaprogrammation, sinon la lecture risque d'être pour le moins ardue.
Évidemment, plusieurs cas plus complexes sont envisageables, surtout si on utilise des traits pour faciliter le choix à la compilation des meilleures méthodes dans certaines circonstances.
Envisageons une classe Machin, générique sur un type T, pour laquelle certaines contraintes s'appliquent :
L'idée ici est de couvrir des points de syntaxe peu connus pour vous aider si vous en avez besoin un de ces quatre. Voyons comment exprimer le tout. Du point de vue des déclarations, nous aurons :
//
// Classe générique Machin
//
template <class T>
class Machin
{
public:
//
int SeulementSiTEstGrosCommeUnShort (int);
//
template <class U>
int DevraitPasser(U, int);
//
int DevraitPasser(short, int);
//
template <class U>
struct Bidule
{
int DevraitPasser(char, char, char);
};
};
Du point de vue des définitions, nous aurons ce qui suit. Notez que les paramètres ne portent pas de noms parce que nous ne les utilisons pas ici (ça évite des avertissements à la compilation) mais qu'ils seraient évidemment nommés convenablement si les méthodes devaient les utiliser réellement.
//
template < class T>
int Machin < T >::SeulementSiTEstGrosCommeUnShort(int)
{
static_assert(sizeof(T) == sizeof(short), "Taille incorrecte");
return 3; // juste pour les fins de l'exemple
}
//
template <class T>
template <class U>
int Machin<T>::DevraitPasser(U, int)
{
return 4; // juste pour les fins de l'exemple
}
//
template <class T>
int Machin<T>::DevraitPasser(short, int)
{
return 5; // juste pour les fins de l'exemple
}
//
template <class T>
template <class U>
int Machin<T>::Bidule<U>::DevraitPasser(char, char, char)
{
return 6; // juste pour les fins de l'exemple
}
Petite remarque : si on a une méthode générique sur un type U et une autre du même nom qui n'est pas générique, on fait tout simplement de la surcharge de fonction et celle qui est générique tient lieu de cas général.
Enfin, pour un exemple d'utilisation :
int main()
{
Machin<double> doubleMachin;
Machin<short> shortMachin;
//
// Appel de la fonction à un paramètre depuis un Machin
// valide, générique sur un short
//
int j = shortMachin.DevraitPasser(4);
//
// Appel de la fonction à un paramètre depuis un Machin
// invalide (générique sur un double, et sizeof(double)
// n'a pas la même valeur que sizeof(short)).
//
// Plante à la compilation, grâce à l'assertion statique
// (décommenter pour vérifier)
//
//int k = doubleMachin.DevraitPasser(5);
//
// Appel de la fonction à 2 paramètres, version générique
//
int l = doubleMachin.DevraitPasser(2.0, 3);
//
// Appel de la fonction à 2 paramètres, version spécialisée
// sur un short
//
int m = doubleMachin.DevraitPasser(short{2}, 3);
//
// Création d'un Bidule générique sur un short, puis appel
// de sa méthode à trois paramètres
//
Machin<double>::Bidule<short> shortBiduleDeDoubleMachin;
int o = shortBiduleDeDoubleMachin.DevraitPasser('a', 'b', 'c');
}
Bingo! En espérant que ça rende service. Merci à Vincent Thériault, donc, à la fois pour la question initiale et pour le raffinement un peu pervers de Machin<T>::Bidule<U> qui est sa propre création.
Quelques liens pour en savoir plus.