Metaprogramming and Compile-Time Polymorphism

Learning Objectives

Use type traits, concepts, if constexpr and SFINAE.

Implement CRTP and policy-based design.

Compare with Java annotation processors, C# source generators, Python decorators/metaclasses and TypeScript conditional/mapped types.

Prerequisites: Modules 17, 57.

Intuition and Definition

Metaprogramming is code that generates or manipulates code — at compile time (C++ templates, source generators) or at run time (Python metaclasses and decorators).

Type traits (std::is_integral_v<T>) Query properties of types at compile time Concepts / requires

Constrain templates with readable errors if constexpr Compile-time branches inside templates SFINAE / enable_if

Pre-C++20 overload selection by substitution failure CRTP (curiously recurring template pattern)

class Derived: Base<Derived> — static polymorphism and mixins without virtual calls Policy-based design

Configure a class with policy types (Logger<ConsoleSink, NoLock>)

Build expression trees at compile time to fuse loops (Eigen)

Why It Matters

Compile-time polymorphism gives the flexibility of interfaces with zero run-time overhead — critical in embedded and high-performance code — and moves errors from run time to compile time.

Syntax and Language Notes

# Python: class decorators and metaclasses customise classes at definition time
def register(cls):
    REGISTRY[cls.__name__] = cls
    return cls
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };   // mapped + template literal types

Complete Example

Python metaprogramming at run time — a class decorator that registers plugins, and a metaclass that enforces a rule when classes are *defined*:

#include <iostream>
#include <mutex>
#include <string>
#include <type_traits>

// ---- CRTP: static polymorphism + mixin ----
template <class Derived>
class Comparable {                                         // adds !=, >, <=, >= from < and ==
public:
    friend bool operator!=(const Derived& a, const Derived& b) { return !(a == b); }
    friend bool operator>(const Derived& a, const Derived& b) { return b < a; }
};
struct Version : Comparable<Version> {
    int major, minor;
    Version(int a, int b) : major(a), minor(b) {}
    friend bool operator==(const Version& a, const Version& b) { return a.major == b.major && a.minor == b.minor; }
    friend bool operator<(const Version& a, const Version& b) { return a.major < b.major || (a.major == b.major && a.minor < b.minor); }
};

template <class Derived>
class Sensor {                                             // static interface: no vtable
public:
    double read() { return static_cast<Derived*>(this)->rawRead() * scale(); }
    static constexpr double scale() { return Derived::kScale; }
};
struct Coolant : Sensor<Coolant> { static constexpr double kScale = 0.5; double rawRead() { return 190; } };

// ---- policy-based design ---
…
1.4 > 1.10? 0, 1.4 != 1.10? 1
coolant reading 95 (scaled at compile time by 0.5)
value=42
text
[ecu] value=3.500000
sizeof(Logger<ConsoleSink>) = 1, sizeof(Logger<PrefixSink, MutexLock>) = 8
REGISTRY = {}

def plugin(name):
    def wrap(cls):
        REGISTRY[name] = cls
        return cls
    return wrap

class RequiresRun(type):
    def __new__(mcls, name, bases, ns):
        if bases and "run" not in ns:
            raise TypeError(f"{name} must define run()")
        return super().__new__(mcls, name, bases, ns)

class Task(metaclass=RequiresRun):
    pass

@plugin("flash")
class FlashTask(Task):
    def run(self): return "flashing ECU"

print(REGISTRY["flash"]().run())
try:
    class Broken(Task):
        pass
except TypeError as e:
    print("TypeError at class definition:", e)
flashing ECU
TypeError at class definition: Broken must define run()

Step-by-Step Walkthrough

Comparable<Version> knows the derived type at compile time and implements != and > in terms of the derived == and < — a mixin without virtual functions.

Sensor<Coolant>::read() calls Coolant::rawRead() through static_cast — resolved at compile time; scale() is constexpr.

Logger<Sink, LockPolicy> is assembled from policies; NoLock costs nothing, and if constexpr selects formatting per argument type.

The MutexLock policy makes that logger bigger (the mutex); the NoLock version is an empty class (size 1).

In Python, the decorator runs when the class is defined; the metaclass rejects a class missing run() at definition time, before any instance exists.

Common Mistakes

Template error walls without concepts — constrain early.

Code bloat from many instantiations of large templates.

CRTP misuse: deriving B : Base<A> by mistake (fix with a private constructor + friend Derived).

Metaclass magic in Python that surprises readers — prefer decorators and __init_subclass__.

Trade-offs and Alternatives

Compile-time (templates, CRTP, policies) Run-time (virtuals, DI, reflection) Flexibility after build indirection, allocation run time (or start-up) Compile time / binary size

Java annotation processors and C# source generators generate code at build time from annotations/attributes; TypeScript's conditional and mapped types compute *types* (erased at run time).

Real-World Use

The STL and Boost, Eigen expression templates, embedded HALs configured by policies (Uart<Usart2, Dma1>), Lombok and MapStruct (Java), System.Text.Json source generation (.NET), Django/SQLAlchemy models (metaclasses), Angular decorators.

Practice

Q1. (Easy) What does CRTP stand for?

Answer: Curiously Recurring Template Pattern.

Q2. (Medium) Why is CRTP dispatch faster than virtual dispatch?

Answer: The call target is known at compile time (static_cast to the derived type), so the compiler can inline it; no vptr or indirect call.

Q3. (Advanced) What does __init_subclass__ offer compared with a metaclass?

Answer: A simpler hook (a classmethod on the base class) that runs whenever a subclass is defined — enough for registration and validation without writing a metaclass and without metaclass-conflict problems.

Interview Question

Q1. "When would you choose CRTP/policies over virtual functions?"

Answer: When the variation is fixed at build time (per product variant or configuration), performance matters (hot loops, embedded ISRs), and you don't need heterogeneous collections of different implementations. Virtual functions are better when the implementation is chosen at run time, plug-ins are loaded dynamically, or you need std::vector<std::unique_ptr<Base>>.

Quick Quiz

Q1. What does if constexpr do?

Answer: Chooses a branch at compile time and discards the others.

Q2. Policy-based design configures a class with…?

Answer: Template parameters that supply behaviour (policies).

Q3. When does a Python metaclass's __new__ run?

Answer: When a class using it is defined (class creation time).

Summary

Metaprogramming generates/selects code at compile time (C++, generators) or run time (Python).

CRTP and policies give zero-overhead polymorphism; concepts keep errors readable.

Related: Modules 17, 57, 70.