Die meisten Implementierungen von Generika (oder besser: parametrischer Polymorphismus) verwenden die Typlöschung. Dies vereinfacht das Problem des Kompilierens von generischem Code erheblich, funktioniert jedoch nur für Boxed-Typen: Da jedes Argument effektiv ein undurchsichtiger Zeiger ist, benötigen wir ein VTable oder einen ähnlichen Dispatch-Mechanismus, um Operationen an den Argumenten auszuführen. In Java:
<T extends Addable> T add(T a, T b) { … }
kann wie folgt kompiliert, typgeprüft und aufgerufen werden
Addable add(Addable a, Addable b) { … }
mit der Ausnahme, dass Generika der Typprüfung am Aufrufstandort weitaus mehr Informationen liefern. Diese zusätzlichen Informationen können mit Typvariablen verarbeitet werden , insbesondere wenn generische Typen abgeleitet werden. Während der Typprüfung kann jeder generische Typ durch eine Variable ersetzt werden. Nennen wir es $T1
:
$T1 add($T1 a, $T1 b)
Die Typvariable wird dann mit weiteren bekannten Fakten aktualisiert, bis sie durch einen konkreten Typ ersetzt werden kann. Der Typprüfungsalgorithmus muss so geschrieben sein, dass diese Typvariablen berücksichtigt werden, auch wenn sie noch nicht in einen vollständigen Typ aufgelöst sind. In Java selbst ist dies normalerweise einfach möglich, da die Art der Argumente häufig bekannt ist, bevor die Art des Funktionsaufrufs bekannt sein muss. Eine bemerkenswerte Ausnahme ist ein Lambda-Ausdruck als Funktionsargument, der die Verwendung solcher Typvariablen erfordert.
Viel später, ein Optimierer kann spezialisierten Code für einen bestimmten Satz von Argumenten erzeugt, würde dies dann effektiv eine Art inlining.
Eine VTable für generische Argumente kann vermieden werden, wenn die generische Funktion keine Operationen für den Typ ausführt, sondern sie nur an eine andere Funktion übergibt. ZB call :: (a -> b) -> a -> b; call f x = f x
müsste die Haskell-Funktion das x
Argument nicht boxen . Dies erfordert jedoch eine Aufrufkonvention, die Werte ohne Kenntnis ihrer Größe durchlaufen kann, wodurch sie im Wesentlichen ohnehin auf Zeiger beschränkt wird.
C ++ unterscheidet sich in dieser Hinsicht sehr von den meisten Sprachen. Eine Klasse oder Funktion mit Vorlagen (ich werde hier nur auf Funktionen mit Vorlagen eingehen) ist an sich nicht aufrufbar. Stattdessen sollten Vorlagen als Metafunktion zur Kompilierungszeit verstanden werden, die eine tatsächliche Funktion zurückgibt. Wenn Sie die Inferenz der Vorlagenargumente für einen Moment ignorieren, läuft der allgemeine Ansatz auf die folgenden Schritte hinaus:
Wenden Sie die Vorlage auf die angegebenen Vorlagenargumente an. Wenn Sie beispielsweise template<class T> T add(T a, T b) { … }
as aufrufen add<int>(1, 2)
, erhalten Sie die eigentliche Funktion int __add__T_int(int a, int b)
(oder wie auch immer der Name-Mangling-Ansatz lautet).
Wenn in der aktuellen Kompilierungseinheit bereits Code für diese Funktion generiert wurde, fahren Sie fort. Generieren Sie den Code ansonsten so, als ob eine Funktion int __add__T_int(int a, int b) { … }
in den Quellcode geschrieben worden wäre. Dies beinhaltet das Ersetzen aller Vorkommen des Vorlagenarguments durch seine Werte. Dies ist wahrscheinlich eine AST → AST-Transformation. Führen Sie dann eine Typprüfung des generierten AST durch.
Kompilieren Sie den Aufruf, als ob der Quellcode gewesen wäre __add__T_int(1, 2)
.
Beachten Sie, dass C ++ - Vorlagen eine komplexe Interaktion mit dem Überladungsauflösungsmechanismus haben, den ich hier nicht beschreiben möchte. Beachten Sie auch, dass diese Codegenerierung es unmöglich macht, eine Vorlage zu haben, die auch virtuell ist - ein Ansatz auf der Basis von Typlöschung leidet nicht unter dieser wesentlichen Einschränkung.
Was bedeutet das für Ihren Compiler und / oder Ihre Sprache? Sie müssen sorgfältig überlegen, welche Arten von Generika Sie anbieten möchten. Die Typlöschung ohne Typinferenz ist der einfachste Ansatz, wenn Sie Boxed Types unterstützen. Die Template-Spezialisierung scheint recht einfach zu sein, beinhaltet jedoch in der Regel eine Namensverknüpfung und (bei mehreren Kompilierungseinheiten) eine erhebliche Verdoppelung der Ausgabe, da Templates am Aufrufstandort und nicht am Definitionsstandort instanziiert werden.
Der Ansatz, den Sie gezeigt haben, ist im Wesentlichen ein C ++ - ähnlicher Template-Ansatz. Sie speichern die spezialisierten / instanziierten Vorlagen jedoch als „Versionen“ der Hauptvorlage. Dies ist irreführend: Sie sind konzeptionell nicht identisch, und verschiedene Instanzen einer Funktion können sehr unterschiedliche Typen haben. Dies wird die Dinge auf lange Sicht komplizieren, wenn Sie auch eine Funktionsüberladung zulassen. Stattdessen benötigen Sie den Begriff eines Überladungssatzes, der alle möglichen Funktionen und Vorlagen enthält, die einen gemeinsamen Namen haben. Mit Ausnahme des Behebens von Überladungen können Sie verschiedene instanziierte Vorlagen als vollständig voneinander getrennt betrachten.