La mayoría de las implementaciones de genéricos (o más bien: polimorfismo paramétrico) usan borrado de tipo. Esto simplifica enormemente el problema de compilar código genérico, pero solo funciona para tipos encuadrados: dado que cada argumento es efectivamente un puntero opaco, necesitamos un VTable o un mecanismo de envío similar para realizar operaciones en los argumentos. En Java:
<T extends Addable> T add(T a, T b) { … }
se puede compilar, verificar y escribir de la misma manera que
Addable add(Addable a, Addable b) { … }
excepto que los genéricos proporcionan al verificador de tipos mucha más información en el sitio de la llamada. Esta información adicional se puede manejar con variables de tipo , especialmente cuando se infieren tipos genéricos. Durante la verificación de tipos, cada tipo genérico se puede reemplazar con una variable, llamémoslo $T1
:
$T1 add($T1 a, $T1 b)
La variable de tipo se actualiza con más hechos a medida que se conocen, hasta que se pueda reemplazar por un tipo concreto. El algoritmo de verificación de tipo debe escribirse de manera que acomode estas variables de tipo incluso si aún no se han resuelto en un tipo completo. En Java, esto generalmente se puede hacer fácilmente ya que el tipo de argumentos a menudo se conoce antes de que sea necesario conocer el tipo de llamada a la función. Una excepción notable es una expresión lambda como argumento de función, que requiere el uso de tales variables de tipo.
Mucho más tarde, un optimizador puede generar código especializado para un cierto conjunto de argumentos, esto sería efectivamente una especie de línea.
Se puede evitar una VTable para argumentos de tipo genérico si la función genérica no realiza ninguna operación en el tipo, sino que solo los pasa a otra función. Por ejemplo, la función Haskell call :: (a -> b) -> a -> b; call f x = f x
no tendría que encuadrar el x
argumento. Sin embargo, esto requiere una convención de llamada que pueda pasar valores sin conocer su tamaño, lo que esencialmente lo restringe a los punteros de todos modos.
C ++ es muy diferente de la mayoría de los lenguajes a este respecto. Una clase o función con plantilla (solo discutiré funciones con plantilla aquí) no es invocable en sí misma. En cambio, las plantillas deben entenderse como una metafunción en tiempo de compilación que devuelve una función real. Ignorando la inferencia de argumentos de plantilla por un momento, el enfoque general se reduce a estos pasos:
Aplique la plantilla a los argumentos de plantilla proporcionados. Por ejemplo, llamar template<class T> T add(T a, T b) { … }
como add<int>(1, 2)
nos daría la función real int __add__T_int(int a, int b)
(o cualquier enfoque de cambio de nombre utilizado).
Si el código para esa función ya se ha generado en la unidad de compilación actual, continúe. De lo contrario, genere el código como si se int __add__T_int(int a, int b) { … }
hubiera escrito una función en el código fuente. Esto implica reemplazar todas las apariciones del argumento de plantilla con sus valores. Esta es probablemente una transformación AST → AST. Luego, realice una verificación de tipo en el AST generado.
Compila la llamada como si lo hubiera sido el código fuente __add__T_int(1, 2)
.
Tenga en cuenta que las plantillas de C ++ tienen una interacción compleja con el mecanismo de resolución de sobrecarga, que no quiero describir aquí. También tenga en cuenta que esta generación de código hace que sea imposible tener un método con plantilla que también sea virtual: un enfoque basado en el borrado de tipo no sufre esta restricción sustancial.
¿Qué significa esto para su compilador y / o idioma? Tienes que pensar cuidadosamente sobre el tipo de genéricos que quieres ofrecer. El borrado de tipo en ausencia de inferencia de tipo es el enfoque más simple posible si admite tipos en caja. La especialización de la plantilla parece bastante simple, pero generalmente implica el cambio de nombre y (para múltiples unidades de compilación) una duplicación sustancial en la salida, ya que las plantillas se instancian en el sitio de la llamada, no en el sitio de definición.
El enfoque que ha mostrado es esencialmente un enfoque de plantilla similar a C ++. Sin embargo, almacena las plantillas especializadas / instanciadas como "versiones" de la plantilla principal. Esto es engañoso: no son los mismos conceptualmente, y las diferentes instancias de una función pueden tener tipos muy diferentes. Esto complicará las cosas a largo plazo si también permite la sobrecarga de funciones. En cambio, necesitaría una noción de un conjunto de sobrecarga que contiene todas las funciones y plantillas posibles que comparten un nombre. Excepto para resolver la sobrecarga, puede considerar que diferentes plantillas instanciadas estén completamente separadas entre sí.