Most implementations of generics (or rather: parametric polymorphism) do use type erasure. This greatly simplifies the problem of compiling generic code, but only works for boxed types: since each argument is effectively an opaque pointer, we need a VTable or similar dispatch mechanism to perform operations on the arguments. In Java:
<T extends Addable> T add(T a, T b) { … }
can be compiled, type-checked, and called the same way as
Addable add(Addable a, Addable b) { … }
except that generics provide the type checker with far more information at the call site. This extra information can be handled with type variables, especially when generic types are inferred. During type checking, each generic type can be replaced with a variable, let's call it $T1
:
$T1 add($T1 a, $T1 b)
The type variable is then updated with more facts as they become known, until it can be replaced with a concrete type. The type checking algorithm must be written in a way that accommodates these type variables even if they are not yet resolved to a complete type. In Java itself this can usually be done easily since the type of the arguments is often known before the type of the function call needs to be known. A notable exception is a lambda expression as function argument, which requires the use of such type variables.
Much later, an optimizer may generate specialized code for a certain set of arguments, this would then effectively be a kind of inlining.
A VTable for generic-typed arguments can be avoided if the generic function does not perform any operations on the type, but only passes them to another function. E.g. the Haskell function call :: (a -> b) -> a -> b; call f x = f x
would not have to box the x
argument. However, this does requires a calling convention that can pass through values without knowing their size, which essentially restricts it to pointers anyway.
C++ is very different from most languages in this respect. A templated class or function (I'll only discuss templated functions here) is not callable in itself. Instead, templates should be understood as a compile-time meta-function that returns an an actual function. Ignoring template argument inference for a moment, the general approach then boils down to these steps:
Apply the template to the provided template arguments. E.g calling template<class T> T add(T a, T b) { … }
as add<int>(1, 2)
would give us the actual function int __add__T_int(int a, int b)
(or whatever name-mangling approach is used).
If code for that function has already been generated in the current compilation unit, continue. Otherwise, generate the code as if a function int __add__T_int(int a, int b) { … }
had been written in the source code. This involves replacing all occurrences of the template argument with its values. This is probably a AST→AST transformation. Then, perform type checking on the generated AST.
Compile the call as if the source code had been __add__T_int(1, 2)
.
Note that C++ templates have a complex interaction with the overload resolution mechanism, which I do not want to describe here. Also note that this code-generation makes it impossible to have a templated method that is also virtual – a type-erasure based approach does not suffer from this substantial restriction.
What does this mean for your compiler and/or language? You have to think carefully about the kind of generics you want to offer. Type erasure in the absence of type inference is the simplest possible approach if you support boxed types. Template specialization is seems fairly simple, but usually involves name mangling and (for multiple compilation units) substantial duplication in the output, since templates are instantiated at the call site, not the definition site.
The approach you have shown is essentially a C++-like template approach. However, you store the specialized/instantiated templates as “versions” of the main template. This is misleading: they are not the same conceptually, and different instantiations of a function can have wildly different types. This will complicate things in the long run if you also allow function overloading. Instead, you would need a notion of an overload set that contains all possible functions and templates that share a name. Except for resolving overloading, you can consider different instantiated templates to be completely separate from each other.