At first glance, C++ and C# are two very similar languages. However the more you look, the more you realise that they are two very different beasts. Today we are going to talk about how these two languages handle generic programming.
The Implementation
First lets look at how the two languages implement generic programming. In C++ we can define a template for a class like this:
template<typename T>
class Holder
{
public:
T value;
};
We can then use that template in our code like so:
Holder<int> intHolder;
When the compiler sees this usage, it goes back to the definition of the Holder
class and generates a new class. In this new class it replaces the type variable T
with the type int
. This new class generated by the compiler is what the name Holder<int>
refers to. This class gets compiled as normal. It would be completely equivalent if we had instead defined a Holder
class specialised to int
s ourselves:
class Holder_int
{
public:
int value;
};
If we also specialise our Holder
class with the type double
, the compiler will generate another version of Holder
, this time with type variable T
replaced by the type double
.
Now, in C# the equivalent to the above example is:
class Holder<T>
{
public T value;
}
This looks very similar, however the underlying implementation is very different. In C# generics are resolved at runtime not at compile time. The Holder
class we defined will remain generic when it is complied to intermediate language. When we run code that uses the Holder
class, the just in time compiler generates the different machine code implementations for whatever specialisation of Holder
we have used. The JIT compiler will generate separate versions of the Holder
class for each value type used. However, it generates a single implementation that is shared for all reference types. This works because reference types are implemented as pointers, which are always the same size, regardless of the type they are pointing to.
Constraints
Usually we need more than just a typename in our generic code. We want to do things with the values of that type. So, suppose we wanted to add a method to our generic Holder
class that calls a length
method on our inner object of type T
. In C# we do it like this:
public interface IHasLength
{
int length();
};
public class Holder<T> where T : IHasLength
{
public T value;
int GetLength()
{
return T.length();
}
}
The syntax, class Holder where T : IHasLength
specifies that whatever type this generic is specialised with must implement the interface IHasLength
. With that constraint, the compiler knows that whatever type we specialise this class with will have a Length
method, so we can call it freely in our generic code. There are five different kinds of constraint we can impose. We have just seen the interface constraint. We can also constrain a type parameter to be a subclass of a given class, to have a default constructor, to be a value type or to be a reference type.
In C++ we don’t need explicit type constraints. Instead there are implicit type constraints. For example, suppose we have the following code:
template<typename T>
class Holder
{
public:
T value;
int GetLength()
{
return value.length();
}
}
Here, we have just written our code assuming that an object of type T has a Length
method that returns a value that can be cast to an integer. We do not need to specify this constraint ourselves. We can instantiate this class and specify a specific type, for example via:
Holder<string> stringHolder();
The compiler then generates the version of Holder specialised to the type string
. This new class is then compiled as normal. As the string class has a length method, this will compile successfully. However, suppose we instantiated our class like so:
Holder<int> intHolder();
The int
type does not have a length
method, so the class generated by this code will not compile.
Whereas the constraints on C# generic types are quite limited, in C++ we can use any constraint at all. For example, it is impossible to impose a constraint on a generic type parameter in C# that it must have an addition operator. This is because there is no interface that all types that have an addition operator implement. However, in C++, we just use the addition operator in our template code and the compiler will make sure that the specific types we use have an addition operator. This all works in C++ because the types are resolved at compile time, so the compiler can check that types satisfy the implicit interface then. In C# the type substitution does not actually happen until runtime, so it would be impossible for the C# compiler to do a check like this.
Beyond Type Parameters
Another cool feature of C++ templates is that we can pass values to our templates as well as type parameters. For example, in C++ the following code is valid:
tempate<int n>
bool LessThan(int x)
{
return x < n;
}
This defines a template function with a parameter of type int
. We can use this template like so:
bool result = LessThan<10>(5);
The code LessThan<10>
causes the compiler to generate a version of the LessThan
method with 10 substituted for the parameter n
. You can’t do anything like this in C#.
Overall, C++ templates are a lot more powerful and flexible than C# generics. Of course this means that it is a lot easier to trip yourself up with C++ templates. And when things do go wrong the compiler output is probably going to be completely incomprehensible. As C++ generates separate code for every different version of a template that gets used this has the potential to create very large binaries. However, on a modern system this probably won’t really be a problem.