A maioria das implementações de genéricos (ou melhor: polimorfismo paramétrico) usa apagamento de tipo. Isso simplifica bastante o problema de compilar código genérico, mas funciona apenas para tipos in a box: como cada argumento é efetivamente um ponteiro opaco, precisamos de uma VTable ou de um mecanismo de expedição semelhante para executar operações nos argumentos. Em Java:
<T extends Addable> T add(T a, T b) { … }
pode ser compilado, verificado por tipo e chamado da mesma maneira que
Addable add(Addable a, Addable b) { … }
exceto que os genéricos fornecem ao verificador de tipo muito mais informações no site de chamada. Essas informações extras podem ser tratadas com variáveis de tipo , especialmente quando tipos genéricos são inferidos. Durante a verificação de tipo, cada tipo genérico pode ser substituído por uma variável, vamos chamá-lo $T1
:
$T1 add($T1 a, $T1 b)
A variável de tipo é então atualizada com mais fatos à medida que se tornam conhecidos, até que possa ser substituída por um tipo concreto. O algoritmo de verificação de tipo deve ser escrito de forma a acomodar essas variáveis de tipo, mesmo que ainda não tenham sido resolvidas para um tipo completo. No próprio Java, isso geralmente pode ser feito facilmente, já que o tipo de argumento geralmente é conhecido antes que o tipo de chamada de função precise ser conhecido. Uma exceção notável é uma expressão lambda como argumento de função, que requer o uso de tais variáveis de tipo.
Muito mais tarde, um otimizador pode gerar código especializado para um determinado conjunto de argumentos; isso seria efetivamente um tipo de inlining.
Uma VTable para argumentos de tipo genérico pode ser evitada se a função genérica não executar nenhuma operação no tipo, mas apenas as passar para outra função. Por exemplo, a função Haskellcall :: (a -> b) -> a -> b; call f x = f x
não precisaria encaixar o x
argumento. No entanto, isso requer uma convenção de chamada que possa passar por valores sem saber seu tamanho, o que basicamente a restringe a ponteiros de qualquer maneira.
C ++ é muito diferente da maioria dos idiomas a esse respeito. Uma classe ou função modelada (discutirei apenas funções modeladas aqui) não é exigível por si só. Em vez disso, os modelos devem ser entendidos como uma meta-função em tempo de compilação que retorna uma função real. Ignorando a inferência do argumento do modelo por um momento, a abordagem geral se resume a estas etapas:
Aplique o modelo aos argumentos do modelo fornecidos. Por exemplo, chamando template<class T> T add(T a, T b) { … }
comoadd<int>(1, 2)
nos daria a função real int __add__T_int(int a, int b)
(ou qualquer outra abordagem usada para identificar nomes).
Se o código para essa função já tiver sido gerado na unidade de compilação atual, continue. Caso contrário, gere o código como se uma funçãoint __add__T_int(int a, int b) { … }
tivesse sido gravada no código-fonte. Isso envolve substituir todas as ocorrências do argumento do modelo por seus valores. Provavelmente esta é uma transformação AST → AST. Em seguida, execute a verificação de tipo no AST gerado.
Compile a chamada como se o código-fonte tivesse sido __add__T_int(1, 2)
.
Observe que os modelos C ++ têm uma interação complexa com o mecanismo de resolução de sobrecarga, que eu não quero descrever aqui. Observe também que essa geração de código torna impossível um método de modelo virtual também - uma abordagem baseada em apagamento de tipo não sofre com essa restrição substancial.
O que isso significa para o seu compilador e / ou idioma? Você precisa pensar cuidadosamente sobre o tipo de genéricos que deseja oferecer. O apagamento de tipo na ausência de inferência de tipo é a abordagem mais simples possível se você oferecer suporte a tipos in a box. A especialização de modelos parece bastante simples, mas geralmente envolve a troca de nomes e (para várias unidades de compilação) uma duplicação substancial na saída, pois os modelos são instanciados no site de chamada, não no site de definição.
A abordagem que você mostrou é essencialmente uma abordagem de modelo semelhante ao C ++. No entanto, você armazena os modelos especializados / instanciados como "versões" do modelo principal. Isso é enganador: eles não são os mesmos conceitualmente e instâncias diferentes de uma função podem ter tipos totalmente diferentes. Isso complicará as coisas a longo prazo se você também permitir sobrecarga de função. Em vez disso, você precisaria de uma noção de um conjunto de sobrecarga que contenha todas as funções e modelos possíveis que compartilhem um nome. Exceto para resolver a sobrecarga, você pode considerar diferentes modelos instanciados como completamente separados um do outro.