Understanding Generics in C# vs Templates in C++

Myth vs Reality: Two Approaches to Generic Programming

Myth: C# generics and C++ templates are just different syntax for the same concept. Reality: They're fundamentally different mechanisms with distinct compilation models, runtime behavior, and design philosophies. C++ templates perform compile-time code generation and specialization, while C# generics use runtime type substitution within a single compiled implementation.

This difference impacts everything from performance characteristics to what you can express with each system. Templates give you maximum flexibility and compile-time computation at the cost of longer build times and code bloat. Generics give you type safety, faster compilation, and smaller binaries but with stricter constraints on what operations you can perform.

You'll learn how each approach works under the hood, when to use constraints versus template metaprogramming, and how to write type-safe generic code that matches your language's strengths. We'll cover compilation differences, type erasure myths, constraint systems, and practical examples that highlight when each approach shines.

How Compilation Works: Code Generation vs Type Substitution

The core difference between C# generics and C++ templates lies in when and how type information gets resolved. C++ templates are a compile-time mechanism that generates separate code for each type you use. When you instantiate std::vector<int> and std::vector<string>, the compiler literally creates two distinct classes with different machine code.

C# generics work differently. The compiler creates one implementation in IL (Intermediate Language), and the CLR performs type substitution at runtime. When you use List<int> and List<string>, they share the same compiled code with type parameters filled in by the runtime. This means faster compilation and smaller assemblies.

C++ Template - Compile-time code generation
// C++ Example (for comparison)
// template<typename T>
// class Stack {
//     std::vector<T> items;
// public:
//     void Push(T item) { items.push_back(item); }
//     T Pop() {
//         T val = items.back();
//         items.pop_back();
//         return val;
//     }
// };
//
// Stack<int> intStack;      // Generates specialized Stack code for int
// Stack<string> strStack;   // Generates different code for string
// Result: Two distinct compiled implementations in the binary
C# Generic - Runtime type substitution
// C# Generic class
public class Stack<T>
{
    private List<T> items = new();

    public void Push(T item) => items.Add(item);

    public T Pop()
    {
        var item = items[^1];
        items.RemoveAt(items.Count - 1);
        return item;
    }

    public int Count => items.Count;
}

// Usage
var intStack = new Stack<int>();      // Same IL code, T = int at runtime
var strStack = new Stack<string>();  // Same IL code, T = string at runtime
// Result: One implementation in IL, specialized per value type by JIT

The CLR does create specialized JIT-compiled code for each value type (int, double, DateTime) to avoid boxing overhead. For reference types, it reuses the same JIT-compiled code because all references are the same size. This optimization gives you performance close to C++ templates for value types while keeping your assembly small.

Type Safety and Verification: When Errors Get Caught

C++ templates follow a "duck typing" approach where the compiler only verifies your template code when you actually use it with a concrete type. If your template calls obj.DoSomething(), the compiler doesn't check whether that method exists until you instantiate the template. This can lead to cryptic error messages deep in template instantiation stacks.

C# generics are type-checked when you define them, not when you use them. The compiler verifies that your generic code only uses operations permitted by the constraints you specify. This catches errors earlier and produces clearer error messages, but it also means you can't just call arbitrary methods on T without declaring a constraint.

C# - Constraints required for type safety
// This won't compile - can't assume T has CompareTo
// public class Sorter<T>
// {
//     public T Max(T a, T b) => a.CompareTo(b) > 0 ? a : b;  // ERROR
// }

// Correct: Use constraints to declare requirements
public class Sorter<T> where T : IComparable<T>
{
    public T Max(T a, T b) => a.CompareTo(b) > 0 ? a : b;

    public T Min(T a, T b) => a.CompareTo(b) < 0 ? a : b;

    public T[] Sort(T[] items)
    {
        var sorted = items.ToArray();
        Array.Sort(sorted);
        return sorted;
    }
}

// Usage is type-safe at compile time
var sorter = new Sorter<int>();
int maximum = sorter.Max(10, 20);           // Works: int implements IComparable<int>

// This would fail at compile time
// var badSorter = new Sorter<object>();   // ERROR: object doesn't implement IComparable<object>

The constraint system means you can't accidentally write generic code that compiles but fails at runtime. C++ templates give you more flexibility to write code that works with any type that "happens" to have the right methods, but you lose the early verification that catches mistakes before users encounter them.

Constraints in C# vs Concepts in C++

C# uses constraints to specify what operations a generic type must support. You can require that T implements an interface, derives from a base class, has a parameterless constructor, or is a reference/value type. These constraints are checked at compile time and enforced at runtime.

C++20 introduced concepts, which are compile-time predicates that check whether a type satisfies certain requirements. Concepts can express more complex conditions than C# constraints, including checking for specific member functions, operators, or type traits. However, C# 11 narrowed this gap with static abstract interface members.

C# 11 - Static abstract members for operator constraints
// Define interface with operator requirement
public interface IAddable<T> where T : IAddable<T>
{
    static abstract T operator +(T left, T right);
    static abstract T Zero { get; }
}

// Generic method using the interface
public class Calculator
{
    public static T Sum<T>(IEnumerable<T> values) where T : IAddable<T>
    {
        T result = T.Zero;
        foreach (var value in values)
        {
            result = result + value;
        }
        return result;
    }
}

// Implement for custom type
public record Money(decimal Amount, string Currency) : IAddable<Money>
{
    public static Money operator +(Money left, Money right)
    {
        if (left.Currency != right.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return left with { Amount = left.Amount + right.Amount };
    }

    public static Money Zero => new(0, "USD");
}

// Usage
var transactions = new[] { new Money(100, "USD"), new Money(50, "USD") };
var total = Calculator.Sum(transactions);  // Money(150, "USD")

This pattern lets you write generic math operations that work with any type that defines the required operators. Before C# 11, you'd need runtime reflection or dynamic typing to achieve this. The CLR verifies these constraints when loading types, ensuring type safety without template-style code generation.

Choosing the Right Approach

Choose C++ templates when you need maximum compile-time flexibility and are willing to accept longer build times. Templates excel at compile-time computation, type traits, and SFINAE-based overload resolution. If you're writing a high-performance library where every nanosecond counts and you can afford specialized code for each type, templates are your tool.

Choose C# generics when you value fast compilation, smaller binaries, runtime type inspection, and cross-language compatibility. Generics work across all .NET languages (C#, F#, VB.NET) and support reflection on generic types. For typical business applications, web APIs, and most software projects, generics provide better maintainability.

If you're migrating C++ template code to C#, expect to replace template specialization with strategy patterns or explicit overloads. Template metaprogramming should translate to source generators or runtime reflection. The result will compile faster and integrate better with .NET tooling, though you may need to rethink algorithms that relied on compile-time computation.

Modern C# (11 and later) with static abstract interface members has closed many gaps with C++ concepts. You can now express operator requirements, static factory methods, and other compile-time contracts that were previously impossible. This makes C# generics suitable for mathematical libraries, data structures, and other domains that traditionally required template metaprogramming.

Try It Yourself

Build a small generic repository that demonstrates constraints and runtime type preservation. You'll see how C# generics maintain type information at runtime—unlike C++ template instantiations or Java's type erasure.

Steps

  1. Open your terminal and create a new project: dotnet new console -n GenericsDemo
  2. Navigate to the project: cd GenericsDemo
  3. Replace the contents of Program.cs with the code below
  4. Ensure your .csproj file matches the configuration shown
  5. Run the project: dotnet run
Program.cs
// Generic repository with constraints
public interface IEntity
{
    int Id { get; set; }
}

public class Repository<T> where T : IEntity, new()
{
    private readonly List<T> storage = new();

    public void Add(T entity)
    {
        if (entity.Id == 0)
            entity.Id = storage.Count + 1;
        storage.Add(entity);
    }

    public T? GetById(int id) => storage.FirstOrDefault(e => e.Id == id);

    public void PrintTypeInfo()
    {
        Console.WriteLine($"Repository type: {typeof(T).Name}");
        Console.WriteLine($"Is value type: {typeof(T).IsValueType}");
        Console.WriteLine($"Assembly: {typeof(T).Assembly.GetName().Name}");
    }
}

public record Customer(string Name) : IEntity
{
    public int Id { get; set; }
}

// Demo
var repo = new Repository<Customer>();
repo.Add(new Customer("Alice"));
repo.Add(new Customer("Bob"));

var customer = repo.GetById(1);
Console.WriteLine($"Found: {customer?.Name}");

repo.PrintTypeInfo();
GenericsDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
</Project>

What you'll see

Found: Alice
Repository type: Customer
Is value type: False
Assembly: GenericsDemo

When Not to Use Generics Over Templates

Don't reach for C# generics when you need compile-time code generation or metaprogramming. If your algorithm requires different implementations based on type characteristics—like choosing a sort algorithm based on whether a type is trivially copyable—C++ templates with SFINAE or concepts are the better choice. C# source generators can handle some scenarios, but they work at a different abstraction level.

Avoid C# generics if you need template specialization. C++ lets you provide optimized implementations for specific types (like a specialized vector<bool>). In C#, you'd need separate classes or runtime type checks. This can make certain optimizations awkward to express.

Skip generics when interfacing with native code or when you need precise control over memory layout. C++ templates give you guarantees about struct sizes and offsets at compile time. C# generics don't provide the same level of control for low-level programming, though Unsafe APIs and Span<T> can help in some scenarios.

If your team is migrating a large C++ codebase that relies heavily on template metaprogramming (like Boost.MPL or compile-time expression templates), a direct port to C# generics will be frustrating. Consider redesigning around runtime polymorphism, source generators, or keeping performance-critical template code in native libraries accessed via P/Invoke. The translation isn't always one-to-one, and forcing templates into generics usually produces awkward code.

Quick FAQ

Are C# generics as fast as C++ templates?

C++ templates generate specialized code for each type at compile time, which can be faster. C# generics use runtime type substitution with one compiled version. For value types, .NET creates specialized implementations, narrowing the gap. Choose templates for maximum performance; choose generics for cleaner binaries and type safety.

Can C# generics do template metaprogramming?

No. C++ templates enable compile-time computation and code generation. C# generics are resolved at runtime by the CLR and lack metaprogramming features like template specialization or SFINAE. For compile-time logic in .NET, use source generators instead.

Why can't I use operators with C# generics directly?

C# requires explicit constraints (where T : IComparable<T>) because generics preserve type information at runtime. C++ templates don't verify operators until instantiation. In C# 11+, use static abstract interface members for operator constraints.

What's the gotcha with generic type erasure?

Unlike Java, C# does NOT erase generic type information at runtime. You can use typeof(List<int>) and reflection to inspect T. C++ templates erase after instantiation. This runtime preservation enables better debugging and type checks.

When should I prefer C# generics over templates?

Use C# generics when you need cross-language compatibility, smaller binaries, runtime type checks, or faster compilation. Templates suit performance-critical code requiring compile-time specialization. For typical business apps, generics win on maintainability.

Do C# generic constraints work like C++ concepts?

Partially. Both enforce type requirements, but C# constraints are simpler (interface, base class, new()). C++20 concepts support complex compile-time predicates. C# 11 added static abstract members to interfaces, closing the gap for operator and static method constraints.

Back to Articles