BasicsImpCodeStory_Generics.html
copyright © James Fawcett
Revised: 05/11/2026
11.0 Prologue
Generics allow one implementation to work correctly for many types
without duplication. Polymorphism is the broader ability to write
code that operates uniformly over values of different types. The two mechanisms differ
in when the type is resolved: at compile time (monomorphization, templates)
or at runtime (dynamic dispatch, virtual functions).
11.1 Generic Types and Functions
Generic parameters are placeholders for types supplied by the caller.
// Rust: angle-bracket type parameter
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut l = &list[0];
for item in list { if item > l { l = item; } }
l
}
struct Stack<T> { items: Vec<T> }
// C++: template
template<typename T>
T largest(const std::vector<T>& v) { /* ... */ }
template<typename T>
class Stack { std::vector<T> items; };
// C#: generic type parameter (similar syntax to Java)
T Largest<T>(T[] list) where T : IComparable<T> { /* ... */ }
class Stack<T> { List<T> items = new(); }
// Python: typing module (checked by mypy, not runtime)
from typing import TypeVar, Sequence
T = TypeVar('T')
def largest(lst: Sequence[T]) -> T: ...
11.2 Traits and Interfaces
A trait (Rust) or interface (C#, Java) declares a
set of methods a type must provide. This is the primary mechanism for
ad-hoc polymorphism: defining behavior that multiple unrelated types share.
// Rust: trait definition and implementation
trait Summary {
fn summarize(&self) -> String;
}
impl Summary for Article {
fn summarize(&self) -> String { format!("{}: {}", self.title, self.author) }
}
// C#: interface
interface ISummary {
string Summarize();
}
class Article : ISummary {
public string Summarize() => $"{Title}: {Author}";
}
// C++: abstract base class with pure virtual function
class ISummary {
public:
virtual std::string summarize() const = 0;
virtual ~ISummary() = default;
};
Rust traits support default method implementations, allowing a trait
to provide a fallback that concrete types may override. C# interfaces (C# 8+) also
support default implementations. C++ abstract classes can provide partial implementations.
11.3 Trait Bounds
Trait bounds constrain which types may be substituted for a type
parameter. They state “this generic code works for any type that implements
these capabilities.”
// Rust: inline bound syntax
fn print<T: Display + Debug>(val: T) { println!("{val:?}"); }
// Rust: where clause (preferred for complex bounds)
fn compare<T>(a: T, b: T) -> bool
where T: PartialOrd + Clone
{ a > b }
// C++: concepts (C++20; earlier: SFINAE / enable_if)
template<typename T>
requires std::totally_ordered<T>
bool compare(T a, T b) { return a > b; }
// C#: generic constraints
T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;
Bounds are checked at the call site: the compiler rejects instantiations for types
that do not satisfy the bounds, producing clear error messages. C++ concepts (C++20)
improve on SFINAE by producing readable diagnostics instead of cryptic substitution
failures.
11.4 Dynamic vs Static Dispatch
Static dispatch (monomorphization): the compiler generates a
separate copy of the function for each concrete type. No runtime overhead; larger
binary; type must be known at compile time.
Dynamic dispatch: a vtable (virtual function table) pointer is
stored with the object. The correct implementation is selected at runtime. Enables
heterogeneous collections; adds one indirection per call.
// Rust: static dispatch (impl Trait / generic)
fn draw(shape: &impl Shape) { shape.draw(); } // monomorphized
// Rust: dynamic dispatch (dyn Trait)
fn draw(shape: &dyn Shape) { shape.draw(); } // vtable lookup
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle), Box::new(Rect)];
// C++: virtual functions = dynamic dispatch
class Shape { virtual void draw() const = 0; };
void render(const Shape* s) { s->draw(); } // vtable call
// C#: interface reference = dynamic dispatch
void Draw(IShape s) { s.Draw(); }
Rust makes the choice explicit: impl Trait is static; dyn Trait
is dynamic. C++ virtual functions are always dynamic; non-virtual calls are always static.
C# interface calls are dynamic unless the compiler can devirtualize.
11.5 Variance
Variance describes how subtyping of a generic type relates to
subtyping of its type argument.
-
Covariant in
T: if Dog <: Animal
then Container<Dog> <: Container<Animal>.
Safe for read-only producers.
-
Contravariant in
T: if Dog <: Animal
then Consumer<Animal> <: Consumer<Dog>.
Safe for write-only consumers.
-
Invariant in
T: no subtyping relationship is
induced. Required for mutable containers.
C# generic types are invariant by default; out T marks a type parameter
covariant and in T marks it contravariant (for interfaces and delegates
only). Java arrays are (unsafely) covariant. Rust infers variance from how the type
parameter is used, applying the correct variance automatically.
11.6 Epilogue
Generics eliminate copy-paste code while preserving type safety. Choosing between
static and dynamic dispatch is a performance and API-design decision: static dispatch
is faster but commits to concrete types at compile time; dynamic dispatch enables
runtime flexibility at the cost of a vtable indirection. The final chapter examines
metaprogramming: code that writes or inspects code.
11.7 References
Rust Generics - The Book
C++ Templates - cppreference
C# Generics - Microsoft
Python Generics - mypy