Métaprogrammation avec des patronsLa métaprogrammation avec des patrons est une technique de programmation informatique[1] dans laquelle les patrons sont utilisés de sorte que le compilateur, lors de la compilation du code, exécute un programme. Ces programmes peuvent générer des constantes ou des structures de données. Cette technique est utilisée principalement dans le langage de programmation C++[1]. Calcul de constantesL'exemple simple de calcul de factorielle avec récursion illustre bien ce qu'est la « programmation lors de la compilation ». En C++, une fonction factorielle peut être écrite récursivement comme suit : Exemple :
int factorielle(unsigned int n)
{
if (n == 0)
return 1;
return n * factorielle(n - 1);
}
// factorielle(4) == (4 * 3 * 2 * 1) == 24
// factorielle(0) == 0! == 1
En utilisant la métaprogrammation avec des patrons, on peut écrire: Exemple :
template <unsigned int N>
struct Factorielle
{
enum { valeur = N * Factorielle<N - 1>::valeur };
};
template <>
struct Factorielle<0>
{
enum { valeur = 1 };
};
// Factorielle<4>::valeur == 24
// Factorielle<0>::valeur == 1
La solution de la métaprogrammation avec des patrons utilise la spécialisation de patron pour terminer la récursion. Bien que ces deux solutions soient similaires, dans le deuxième cas, En D, le patron factorielle ressemblerait à : Exemple :
template Factorielle(unsigned int n)
{
static if (n == 1)
const Factorielle = 1;
else
const Factorielle = n * Factorielle!(n-1);
}
// Factorielle!(4) == 24
La métaprogrammation avec des patrons a des utilisations pratiques malgré son apparence maladroite. Elle peut être utilisée pour créer des classes vecteur à n dimensions quand n est connu à la compilation. L'avantage par rapport à un vecteur à n dimensions traditionnel est que les boucles peuvent être déroulées, ce qui produit un code très optimisé. Considérons cet exemple de l'opérateur d'addition. Une addition pour un vecteur à n dimensions pourrait être écrite ainsi : Exemple :
template<int dimension>
Vector<dimension>& Vector<dimension>::operator+=(const Vector<dimension>& rhs)
{
for (int i = 0; i < dimension; i++)
value[i] += rhs.value[i];
return *this;
}
Quand le compilateur instancie la fonction patron définie ci-dessus, le code suivant est produit : Exemple :
template<>
Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs)
{
value[0] += rhs.value[0];
value[1] += rhs.value[1];
return *this;
}
L'optimiseur du compilateur est capable de dérouler la boucle for car le paramètre dimension du patron est une constante connue à la compilation. Pour une véritable implémentation de cette technique, voir (en) [1]. En C++, la métaprogrammation avec des patrons est Turing-complète, ce qui signifie que n'importe quel calcul exprimable par un programme peut être calculé par un métaprogramme à base de patrons. Les métaprogrammes à base de patrons n'ont pas de variables mutables, c'est-à-dire qu'aucune variable ne peut changer de valeur une fois qu'elle a été initialisée. Cela a pour conséquence que contrairement au C++ à l'exécution, la métaprogrammation à base de patrons est une forme de programmation fonctionnelle. Pour cette raison, l'analyse de flot dans les métaprogrammes est faite par le moyen de la récursion, comme vu plus haut Initialisation de donnéesEn C++, la métaprogrammation avec des patrons permet d'initialiser une donnée comme un tableau ou une structure de données complexe. L'exemple suivant illustre le remplissage d'un tableau dont les 32 premières valeurs sont le carré de l'index, et les valeurs suivantes sont l'index. Le template est utilisé pour faire calculer la valeur d'un élément du tableau : Exemple :
template <int N>
struct ValeurTab
{
enum { valeur = N < 32 ? N * N : N };
};
Comme dans l'exemple de factorielle, pour récupérer la 10e valeur, il suffit d'écrire Exemple :
int tab[] = {
ValeurTab<0>::valeur,
ValeurTab<1>::valeur,
ValeurTab<2>::valeur /* ... */
}
Pour faciliter le remplissage d'un grand tableau, on peut utiliser des macros classiques: Exemple :
#define REMPLIR_TAB1(I) ValeurTab<(I)>::valeur
#define REMPLIR_TAB2(I) REMPLIR_TAB1((I)), REMPLIR_TAB1(I + 1)
#define REMPLIR_TAB4(I) REMPLIR_TAB2(I), REMPLIR_TAB2(I + 2)
#define REMPLIR_TAB8(I) REMPLIR_TAB4(I), REMPLIR_TAB4(I + 4)
#define REMPLIR_TAB16(I) REMPLIR_TAB8(I), REMPLIR_TAB8(I + 8)
#define REMPLIR_TAB32(I) REMPLIR_TAB16(I), REMPLIR_TAB16(I + 16)
#define REMPLIR_TAB64(I) REMPLIR_TAB32(I), REMPLIR_TAB32(I + 32)
#define REMPLIR_TAB128(I) REMPLIR_TAB64(I), REMPLIR_TAB64(I + 64)
//...
Ainsi pour remplir un tableau de 100 éléments en respectant l'algorithme du patron de classe, il suffit d'écrire : Exemple :
int tab[] = { REMPLIR_TAB64(0),REMPLIR_TAB32(64),REMPLIR_TAB4(64 + 32) };
Contrôle sur la compilationD'abord, quelques classes : Exemple :
class UneClasseA
{
public:
UneClasseA() {}
virtual ~UneClasseA() {}
};
class UneClasseB : public UneClasseA
{
public:
UneClasseB() {}
virtual ~UneClasseB() {}
};
class UneClasseC : public UneClasseA
{
public:
UneClasseC() {}
virtual ~UneClasseC() {}
};
class UneNouvelleClasseD : public UneClasseA
{
public:
UneNouvelleClasseD() {}
virtual ~UneNouvelleClasseD() {}
};
Il se peut que, dans un programme, un objet de classe Exemple :
UneClasseB* pConvB = 0;
UneClasseC* pConvC = 0;
if (pConvB = dynamic_cast<UneClasseB*>(pObjetA))
{
// Traitement spécifique à uneClasseB
}
else if (pConvC = dynamic_cast<UneClasseC*>(pObjetA))
{
// Traitement spécifique à uneClasseC
}
else
{
// Erreur, type inconnu
}
Un problème peut se poser lors de la programmation d'un nouvel objet ( Ici, la métaprogrammation permet de rendre le programme impossible à compiler si l'on rajoute un type qui n'est traité nulle part. Pour cela, il faut établir une liste de types qui permettra de vérifier si tous les traitements de chaque type ont été programmés. Exemple :
template <class H, class SousListe> struct ListeType {};
struct ListeFin {};
Une liste de types se présentera sous une forme récursive : une liste contient un type et une sous-liste. La liste se termine par le type Exemple :
ListeType<UneClasseB, ListeType<UneClasseC, ListeFin> >
Il suffit alors de créer une classe abstraite qui sera spécialisée avec cette liste de types. Cette classe sera rendue non abstraite en implémentant une méthode virtuelle pure pour chaque type (une méthode par traitement spécifique). Ainsi, l'oubli de l'implémentation de la méthode d'un type de la liste, génèrera une classe abstraite. Une classe abstraite ne pouvant pas être instanciée, le compilateur retournera une erreur lors de la création de l'objet Visiteur. Il suffira alors d'ajouter l'implémentation de la méthode de traitement du nouveau type, pour enlever le comportement abstrait de la classe et permettre une instanciation. Le compilateur peut alors compiler le programme et aucune méthode d'implémentation ne sera oubliée. Il faut donc créer une classe abstraite visiteur avec une liste quelconque de types : Exemple :
template <class liste> class AbsVisiteur;
// Spécialisation de AbsVisiteur pour le cas où c'est une liste de type ListeType
// Hérite de AbsVisiteur de la sous-liste pour créer l'arbre d'héritage afin
// de remonter au parent pour lancer la visite
template <class H, class SousListe>
class AbsVisiteur< ListeType<H, SousListe> > : public AbsVisiteur<SousListe>
{
public:
// Méthode virtuelle pure qui rend la classe abstraite
virtual void visite(H*) = 0;
template <class Z> void lanceVisite(Z* pZ)
{
H* pConv = 0;
if (pConv = dynamic_cast<H*>(pZ))
{
// Le type courant (dans la liste) est celui de la classe courante
// (dans l'arbre d'héritage)
visite(pConv);
}
else
{
// Le type courant (passé à la fonction) est différent de celui du
// type courant (dans la liste et l'arbre héritage)
// La sous-liste est testée avec la classe parente
AbsVisiteur<SousListe>::lanceVisite(pZ);
}
}
};
// Spécialisation pour le type de fin de liste
// C'est la super-classe
template<>
class AbsVisiteur< ListeFin >
{
public:
template <class Z> void lanceVisite(Z* pZ)
{
// Cette classe est la classe mère de toutes les classes correspondant
// au dernier type de la liste (type qui permet de terminer la liste).
// Donc, ici, toute la liste a été testée, et aucune classe correspondante
// au type de la fonction n'a été trouvée.
// Donc ERREUR le type donné n'est pas dans la liste parcourue
throw "Type introuvable";
}
};
Ensuite, il faut créer la liste de types spécifiques. Exemple :
typedef ListeType<UneClasseB, ListeType<UneClasseC, ListeFin> > UneListeClasses;
Enfin, il faut créer la classe qui implémentera tous les traitements spécifiques : Exemple :
class Visiteur : public AbsVisiteur<UneListeClasses>
{
public:
virtual void visite(UneClasseB * b)
{
// Traitement spécifique à uneClasseB
}
virtual void visite(UneClasseC * c)
{
// "Traitement spécifique à uneClasseC
}
// S'il manque l'implémentation d'une classe de la liste, la classe reste
// abstraite car il reste une méthode virtuelle pure Visit() = 0
};
Ainsi, pour ajouter un nouveau type, il faut l'ajouter dans la liste de types Avantages et inconvénients de la métaprogrammation à base de patrons
Références
Information related to Métaprogrammation avec des patrons |