Code Story

Chapter #11 – Generics & Polymorphism

generic types/functions, traits, bounds, dispatch, variance

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