La maggior parte delle implementazioni dei generici (o meglio: polimorfismo parametrico) usano la cancellazione del tipo. Questo semplifica notevolmente il problema della compilazione di codice generico, ma funziona solo per i tipi inscatolati: poiché ogni argomento è effettivamente un puntatore opaco, abbiamo bisogno di un meccanismo di invio VTable o simile per eseguire operazioni sugli argomenti. In Java:
<T extends Addable> T add(T a, T b) { … }
può essere compilato, controllato per tipo e chiamato allo stesso modo di
Addable add(Addable a, Addable b) { … }
tranne che i generici forniscono al verificatore del tipo molte più informazioni sul sito di chiamata. Queste informazioni extra possono essere gestite con variabili di tipo , specialmente quando si deducono tipi generici. Durante il controllo del tipo, ogni tipo generico può essere sostituito con una variabile, chiamiamolo $T1
:
$T1 add($T1 a, $T1 b)
La variabile type viene quindi aggiornata con più fatti man mano che vengono conosciuti, fino a quando non può essere sostituita con un tipo concreto. L'algoritmo di verifica del tipo deve essere scritto in modo da accogliere queste variabili di tipo anche se non sono ancora state risolte in un tipo completo. Nella stessa Java questo può essere fatto facilmente poiché il tipo di argomenti è spesso noto prima che sia necessario conoscere il tipo di chiamata di funzione. Un'eccezione notevole è un'espressione lambda come argomento di funzione, che richiede l'uso di tali variabili di tipo.
Molto più tardi, un ottimizzatore potrebbe generare un codice specializzato per un certo insieme di argomenti, questo sarebbe effettivamente una sorta di allineamento.
Un VTable per argomenti di tipo generico può essere evitato se la funzione generica non esegue alcuna operazione sul tipo, ma le passa solo a un'altra funzione. Ad esempio, la funzione Haskell call :: (a -> b) -> a -> b; call f x = f x
non dovrebbe racchiudere l' x
argomento. Tuttavia, ciò richiede una convenzione di chiamata che può passare attraverso i valori senza conoscerne le dimensioni, il che essenzialmente lo limita ai puntatori.
Il C ++ è molto diverso dalla maggior parte delle lingue in questo senso. Una classe o una funzione basata su modelli (qui parlerò solo delle funzioni basate su modelli) non è richiamabile in sé. Invece, i template dovrebbero essere intesi come una meta-funzione di compilazione in tempo che restituisce una funzione reale. Ignorando per un momento l'inferenza dell'argomento template, l'approccio generale si riduce a questi passaggi:
Applicare il modello agli argomenti del modello forniti. Ad esempio, chiamare template<class T> T add(T a, T b) { … }
as add<int>(1, 2)
ci darebbe la funzione effettiva int __add__T_int(int a, int b)
(o qualunque approccio di manipolazione del nome venga utilizzato).
Se il codice per quella funzione è già stato generato nell'unità di compilazione corrente, continua. Altrimenti, genera il codice come se una funzione int __add__T_int(int a, int b) { … }
fosse stata scritta nel codice sorgente. Ciò comporta la sostituzione di tutte le occorrenze dell'argomento template con i suoi valori. Questa è probabilmente una trasformazione AST → AST. Quindi, eseguire il controllo del tipo sull'AST generato.
Compilare la chiamata come se fosse stato il codice sorgente __add__T_int(1, 2)
.
Si noti che i modelli C ++ hanno un'interazione complessa con il meccanismo di risoluzione del sovraccarico, che non voglio descrivere qui. Si noti inoltre che questa generazione di codice rende impossibile avere un metodo basato su modelli che è anche virtuale - un approccio basato sulla cancellazione di tipi non soffre di questa sostanziale restrizione.
Cosa significa questo per il tuo compilatore e / o lingua? Devi pensare attentamente al tipo di generici che vuoi offrire. La cancellazione del tipo in assenza di inferenza del tipo è l'approccio più semplice possibile se si supportano i tipi in box. La specializzazione dei modelli sembra abbastanza semplice, ma di solito comporta la modifica del nome e (per più unità di compilazione) una duplicazione sostanziale nell'output, poiché i modelli sono istanziati nel sito di chiamata, non nel sito di definizione.
L'approccio che hai mostrato è essenzialmente un approccio modello simile al C ++. Tuttavia, i modelli specializzati / istanziati vengono archiviati come "versioni" del modello principale. Questo è fuorviante: non sono gli stessi concettualmente e diverse istanze di una funzione possono avere tipi selvaggiamente diversi. Ciò complicherà le cose a lungo termine se si consente anche il sovraccarico delle funzioni. Invece, avresti bisogno di una nozione di un set di sovraccarico che contiene tutte le possibili funzioni e modelli che condividono un nome. Ad eccezione della risoluzione del sovraccarico, è possibile considerare diversi modelli istanziati completamente separati l'uno dall'altro.