La mayoría de las implementaciones de genéricos (o más bien: polimorfismo paramétrico) utilizan el borrado de tipo. Esto simplifica en gran medida el problema de compilar el código genérico, pero solo funciona para tipos encajonados: como 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) { … }
puede compilarse, verificarse y escribirse del mismo modo que
Addable add(Addable a, Addable b) { … }
excepto que los genéricos proporcionan al comprobador 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 deducen 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 datos a medida que se conocen, hasta que se puede reemplazar por un tipo concreto. El algoritmo de verificación de tipo debe escribirse de manera que se adapte a estas variables de tipo, incluso si aún no se han resuelto en un tipo completo. En Java mismo, esto generalmente se puede hacer fácilmente ya que el tipo de los argumentos se conoce a menudo antes de que el tipo de la llamada a la función deba ser conocida. 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 determinado conjunto de argumentos, esto sería efectivamente un tipo de integración.
Una VTable para argumentos de tipo genérico se puede evitar si la función genérica no realiza ninguna operación en el tipo, sino que solo pasa a otra función. P.ej. La función de Haskell call :: (a -> b) -> a -> b; call f x = f x
no tendría que marcar el argumento x
. Sin embargo, esto requiere una convención de llamada que pueda pasar a través de los valores sin saber 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 (aquí solo discutiré las funciones con plantilla) no se puede llamar en sí misma. En su lugar, las plantillas deben entenderse como una meta-función en tiempo de compilación que devuelve una función real. Ignorando la inferencia de los argumentos de la plantilla por un momento, el enfoque general se reduce a estos pasos:
-
Aplique la plantilla a los argumentos de plantilla proporcionados. Por ejemplo, llamar a template<class T> T add(T a, T b) { … }
as add<int>(1, 2)
nos daría la función real int __add__T_int(int a, int b)
(o cualquier método de identificación de nombres).
-
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 una función int __add__T_int(int a, int b) { … }
se hubiera escrito en el código fuente. Esto implica reemplazar todas las apariciones del argumento de la plantilla con sus valores. Esta es probablemente una transformación AST → AST. Luego, realice una comprobación de tipo en el AST generado.
-
Compile la llamada como si el código fuente hubiera sido __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 de plantilla que también sea virtual: un enfoque basado en el borrado de tipo no sufre de 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 tipos en ausencia de inferencia de tipos es el enfoque más simple posible si admite tipos encuadrados. La especialización de la plantilla parece bastante simple, pero por lo general implica la manipulación de nombres y (para múltiples unidades de compilación) una duplicación sustancial en la salida, ya que las plantillas se crean instancias 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 / creadas como "versiones" de la plantilla principal. Esto es engañoso: no son lo mismo 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 su lugar, necesitaría una noción de un conjunto de sobrecarga que contenga todas las funciones y plantillas posibles que comparten un nombre. Excepto por resolver la sobrecarga, puede considerar que las diferentes plantillas instanciadas estén completamente separadas unas de otras.