La plupart des implémentations de génériques (ou plutôt: polymorphisme paramétrique) utilisent l'effacement de type. Cela simplifie considérablement le problème de la compilation de code générique, mais ne fonctionne que pour les types encadrés: puisque chaque argument est effectivement un pointeur opaque, nous avons besoin d'un VTable ou d'un mécanisme de répartition similaire pour effectuer des opérations sur les arguments. En Java:
<T extends Addable> T add(T a, T b) { … }
peut être compilé, vérifié par type et appelé de la même manière que
Addable add(Addable a, Addable b) { … }
sauf que les génériques fournissent au vérificateur de type beaucoup plus d'informations sur le site d'appel. Ces informations supplémentaires peuvent être traitées avec des variables de type , en particulier lorsque des types génériques sont déduits. Lors de la vérification de type, chaque type générique peut être remplacé par une variable, appelons-le $T1
:
$T1 add($T1 a, $T1 b)
La variable de type est ensuite mise à jour avec plus de faits au fur et à mesure qu'ils deviennent connus, jusqu'à ce qu'elle puisse être remplacée par un type concret. L'algorithme de vérification de type doit être écrit d'une manière qui accepte ces variables de type même si elles ne sont pas encore résolues en un type complet. En Java lui-même, cela peut généralement être fait facilement car le type des arguments est souvent connu avant que le type de l'appel de fonction doive être connu. Une exception notable est une expression lambda comme argument de fonction, qui nécessite l'utilisation de telles variables de type.
Bien plus tard, un optimiseur peut générer du code spécialisé pour un certain ensemble d'arguments, ce serait alors effectivement une sorte d'inline.
Un VTable pour les arguments de type générique peut être évité si la fonction générique n'effectue aucune opération sur le type, mais les transmet uniquement à une autre fonction. Par exemple, la fonction Haskell call :: (a -> b) -> a -> b; call f x = f x
n'aurait pas à encadrer l' x
argument. Cependant, cela nécessite une convention d'appel qui peut passer par des valeurs sans connaître leur taille, ce qui la limite essentiellement aux pointeurs de toute façon.
Le C ++ est très différent de la plupart des langages à cet égard. Une classe ou une fonction basée sur un modèle (je ne parlerai ici que des fonctions basées sur un modèle) n'est pas appelable en soi. Au lieu de cela, les modèles doivent être compris comme une méta-fonction au moment de la compilation qui renvoie une fonction réelle. Ignorant l'inférence d'argument de modèle pendant un moment, l'approche générale se résume alors à ces étapes:
Appliquez le modèle aux arguments de modèle fournis. Par exemple, appeler template<class T> T add(T a, T b) { … }
comme add<int>(1, 2)
nous donnerait la fonction réelle int __add__T_int(int a, int b)
(ou quelle que soit l'approche de manipulation de nom utilisée).
Si le code de cette fonction a déjà été généré dans l'unité de compilation actuelle, continuez. Sinon, générez le code comme si une fonction int __add__T_int(int a, int b) { … }
avait été écrite dans le code source. Cela implique de remplacer toutes les occurrences de l'argument de modèle par ses valeurs. Il s'agit probablement d'une transformation AST → AST. Ensuite, effectuez une vérification de type sur l'AST généré.
Compilez l'appel comme si le code source l'avait été __add__T_int(1, 2)
.
Notez que les modèles C ++ ont une interaction complexe avec le mécanisme de résolution de surcharge, que je ne veux pas décrire ici. Notez également que cette génération de code rend impossible d'avoir une méthode basée sur des modèles qui est également virtuelle - une approche basée sur l'effacement de type ne souffre pas de cette restriction substantielle.
Qu'est-ce que cela signifie pour votre compilateur et / ou votre langue? Vous devez réfléchir soigneusement au type de génériques que vous souhaitez offrir. L'effacement de type en l'absence d'inférence de type est l'approche la plus simple possible si vous prenez en charge les types encadrés. La spécialisation des modèles semble assez simple, mais implique généralement un changement de nom et (pour plusieurs unités de compilation) une duplication substantielle dans la sortie, car les modèles sont instanciés sur le site d'appel, pas sur le site de définition.
L'approche que vous avez montrée est essentiellement une approche de modèle de type C ++. Cependant, vous stockez les modèles spécialisés / instanciés en tant que «versions» du modèle principal. C'est trompeur: ils ne sont pas les mêmes conceptuellement, et différentes instanciations d'une fonction peuvent avoir des types très différents. Cela compliquera les choses à long terme si vous autorisez également la surcharge de fonctions. Au lieu de cela, vous auriez besoin d'une notion d'un ensemble de surcharge qui contient toutes les fonctions et modèles possibles qui partagent un nom. À l'exception de la résolution de la surcharge, vous pouvez considérer que différents modèles instanciés sont complètement séparés les uns des autres.