Templates and Generics

Learning Objectives

Write function and class templates with defaults, non-type parameters and specialization.

Constrain templates with C++20 concepts.

Compare C++ templates with Java (erasure), C# (reified), Python (typing) and TypeScript generics.

Choose between static and dynamic polymorphism, and use type erasure.

Prerequisites: Modules 8, 15.

Intuition and Definition

Generic programming writes code once for many types. C++ templates are blueprints the compiler *instantiates* separately for each set of type arguments — fully type-checked and optimized, but each instantiation is separate code. Other languages implement generics differently:

Instantiation (monomorphization) Zero overhead, inlining; larger binaries, compile time, errors at instantiation (improved by concepts)

One compiled class; List<String> and List<Integer> are the same class at run time; no new T(), no primitive type args

Type arguments exist at run time; value types get specialised code

TypeVar/Generic/Protocol for static checkers No run-time enforcement Structural, compile-time only Erased in the emitted JavaScript

Variance: if Cat is a subtype of Animal, is List<Cat> a subtype of List<Animal>? *Covariant* (yes, read-only), *contravariant* (reversed, write-only), *invariant* (no). Java uses wildcards (? extends, ? super — "PECS": producer extends, consumer super), C# uses out/in on interfaces and delegates, C++ templates are invariant.

Why It Matters

Generic containers and algorithms (std::vector<T>, List<T>, sort) are the most reused code in any language. Templates are also how C++ gets abstraction without run-time cost.

Syntax and Language Notes

template <typename T>
T maximum(T a, T b) { return (a < b) ? b : a; }

template <typename T, std::size_t N>          // non-type parameter
struct RingBuffer { T items[N]; };

template <std::totally_ordered T>             // concept constraint
T clampTo(T v, T lo, T hi);
// Java (not run)
static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) < 0 ? b : a; }
static double sum(List<? extends Number> xs) { ... }   // producer extends
static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) < 0 ? b : a;
interface IProducer<out T> { T Get(); }   // covariant
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
    def __init__(self, item: T) -> None: self.item = item
function first<T>(xs: readonly T[]): T | undefined { return xs[0]; }
function prop<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }

Complete Example

A concept violation gives a readable error at the call site:

Type erasure hides the concrete type behind a uniform value-semantic wrapper — std::function is the standard example:

TypeScript generics with keyof and mapped types are checked at compile time and erased at run time:

#include <array>
#include <concepts>
#include <iostream>
#include <string>
#include <type_traits>

template <std::totally_ordered T>
T maximum(T a, T b) { return (a < b) ? b : a; }

// class template with a non-type parameter and a default type argument
template <typename T = double, std::size_t N = 4>
class RingBuffer {
    std::array<T, N> items_{};
    std::size_t next_ = 0, count_ = 0;
public:
    void push(const T& v) { items_[next_] = v; next_ = (next_ + 1) % N; if (count_ < N) ++count_; }
    std::size_t size() const { return count_; }
    T average() const requires std::is_arithmetic_v<T> {     // member only for numbers
        T sum{};
        for (std::size_t i = 0; i < count_; ++i) sum += items_[i];
        return count_ ? sum / static_cast<T>(count_) : T{};
    }
};

// full specialization: describe() for a specific type
template <typename T> std::string describe(const T&) { return "some value"; }
template <> std::string describe<bool>(const bool& b) { return b ? "yes" : "no"; }

// variadic template + fold expression
template <typename... Ts>
auto sumAll(Ts... xs) { return (xs + ...); }

// if constexpr: branch chosen at compile time
template <typename T>
std::string 
…
7 2.5 b
size 4 avg 55
names 1
some value, yes
10.5
integral floating other
#include <concepts>
struct Gauge { int value; };          // no operator<
template <std::totally_ordered T>
T maximum(T a, T b) { return (a < b) ? b : a; }
int main() {
    Gauge a{1}, b{2};
    return maximum(a, b).value;
}
m17_concept_fail.cpp:7:12: error: no matching function for call to 'maximum'
#include <any>
#include <functional>
#include <iostream>
#include <string>
#include <variant>
#include <vector>

struct Doubler { int operator()(int x) const { return 2 * x; } };

int main() {
    std::vector<std::function<int(int)>> steps{
        Doubler{},                                  // function object
        [](int x) { return x + 1; },                // lambda
        [offset = 10](int x) { return x - offset; } // lambda with state
    };
    int v = 5;
    for (const auto& f : steps) v = f(v);
    std::cout << "pipeline result " << v << "\n";

    std::variant<int, std::string> cell = std::string("N/A");    // closed set of types
    std::cout << "variant holds string: " << std::holds_alternative<std::string>(cell) << "\n";

    std::any anything = 3.14;                                    // open set, checked at run time
    try { std::cout << std::any_cast<int>(anything); }
    catch (const std::bad_any_cast&) { std::cout << "any holds a double, not an int\n"; }
}
pipeline result 1
variant holds string: 1
any holds a double, not an int
interface Sensor { id: string; celsius: number; ok: boolean; }

function pluck<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
  return items.map((i) => i[key]);
}
type ReadonlyDeep<T> = { readonly [P in keyof T]: T[P] };   // mapped type

const sensors: ReadonlyDeep<Sensor>[] = [
  { id: "coolant", celsius: 92, ok: true },
  { id: "oil", celsius: 104, ok: false },
];
console.log(pluck(sensors, "celsius"));
console.log(pluck(sensors, "id").join(","));
// pluck(sensors, "pressure");   // compile error: "pressure" is not a key of Sensor
[ 92, 104 ]
coolant,oil

Step-by-Step Walkthrough

maximum(3, 7) instantiates maximum<int>; with strings it instantiates maximum<std::string> — two separate functions.

RingBuffer<> uses both default arguments. After five pushes into four slots, the first value was overwritten: (52+54+56+58)/4 = 55.

average() has a requires clause: it exists only when T is arithmetic, so RingBuffer<std::string> compiles as long as you don't call it.

The explicit specialization describe<bool> replaces the generic version for bool.

sumAll(1, 2, 3, 4.5) folds with +; the result type is double.

if constexpr discards the other branches at compile time.

The concept failure is reported at the call: Gauge does not satisfy totally_ordered.

Common Mistakes

Unconstrained templates that produce pages of errors deep inside the implementation — add concepts.

Defining templates in .cpp files — they must be visible (usually in headers) where they are instantiated.

Java: expecting new T(), T.class or instanceof List<String> — impossible after erasure; raw types and unchecked casts cause *heap pollution*.

TypeScript: expecting generics to validate data at run time (JSON.parse returns any regardless of <T>).

Code bloat from many instantiations of large templates.

Trade-offs and Alternatives

Static (templates/CRTP) Dynamic (virtual) Type erasure (std::function, any) indirect call + possible allocation yes (pointers) Compile time / binary size

Real-World Use

The STL, Boost, Eigen, std::chrono, Java collections, .NET LINQ, TypeScript's DOM typings and React's component props all rely on generics. Embedded C++ uses templates for zero-overhead register access and fixed-size buffers (RingBuffer<CanFrame, 64>).

Practice

Q1. (Easy) What is a non-type template parameter?

Answer: A compile-time value parameter such as an integer size (template <std::size_t N>).

Q2. (Medium) Explain PECS with an example.

Answer: "Producer extends, consumer super": a method that only *reads* numbers from a list takes List<? extends Number> (it can accept List<Integer>); a method that only *writes* integers into a list takes List<? super Integer> (it can accept List<Number> or List<Object>).

Q3. (Advanced) Write a concept HasArea and a function totalArea that works for any container of types with area(), without virtual functions.

#include <concepts>
#include <iostream>
#include <vector>
template <typename T>
concept HasArea = requires(const T& t) { { t.area() } -> std::convertible_to<double>; };
struct Square { double s; double area() const { return s * s; } };
struct Disc   { double r; double area() const { return 3.0 * r * r; } };   // rough pi
template <HasArea T>
double totalArea(const std::vector<T>& items) {
    double t = 0;
    for (const auto& i : items) t += i.area();
    return t;
}
int main() {
    std::cout << totalArea(std::vector<Square>{{2}, {3}}) << " " << totalArea(std::vector<Disc>{{1}, {2}}) << "\n";
}

Interview Question

Q1. "Compare C++ templates with Java generics." Answer: C++ templates are instantiated per type argument at compile time: each vector<int> and vector<string> is separate code, types are fully known, primitives work directly, and there is no run-time cost — but binaries and compile times grow, and errors used to be cryptic (concepts fix much of that). Java generics use type erasure: one class at run time, type parameters replaced by their bounds (usually Object) with casts inserted by the compiler; you can't use primitives as type arguments (boxing instead), can't new T(), and can't query List<String> vs List<Integer> at run time. Java's bounded wildcards give use-site variance; C++ templates are invariant.

Quick Quiz

Q1. What does a concept do?

Answer: Names a set of compile-time requirements on template arguments; violating it gives an error at the use site.

Q2. Are TypeScript generic types available at run time?

Answer: No, they are erased.

Q3. Which C# keyword marks a covariant type parameter?

Answer: out.

Summary

Templates instantiate per type (zero overhead); concepts constrain them.

Generic models differ: erasure (Java), reification (C#), static-only (Python, TypeScript).

Choose static, dynamic or type-erased polymorphism deliberately.

Related: Module 8, Module 58 (metaprogramming), Module 82 (cross-language comparison).