Skip to content

Template Metaprogramming

Dunkansdk edited this page Dec 8, 2025 · 1 revision

Template Metaprogramming

Table of Contents

Overview

Template metaprogramming is compile-time code generation using C++ templates. The another-dunkan-engine leverages this technique to:

  • Build type-safe abstractions with zero runtime overhead
  • Transform type lists into storage structures automatically
  • Generate component IDs and bitmasks at compile-time
  • Eliminate runtime type checks and virtual function calls

Key Benefits

Zero Runtime Cost - All computations happen during compilation
Type Safety - Invalid component access caught at compile-time
Code Generation - Automatically create storage tuples, masks, etc.
Flexibility - Add new components without modifying core ECS code

Typelist Fundamentals

A Typelist is a template container that holds types instead of values:

namespace META_TYPES {
    template <typename... TYPES>
    struct Typelist {
        // ... compile-time operations ...
    };
}

Example Usage

// Define component types
using MyComponents = Typelist<PhysicsComponent, RenderComponent, LightComponent>;

// Query at compile-time
static_assert(MyComponents::size() == 3);
static_assert(MyComponents::contains<PhysicsComponent>());
static_assert(MyComponents::pos<RenderComponent>() == 1);

Typelist Architecture

graph TB
    subgraph "Typelist Definition"
        TL["Typelist#60;PhysicsComponent, RenderComponent, LightComponent#62;"]
    end
    
    subgraph "Compile-time Operations"
        Size["size#40;#41; → 3"]
        Contains["contains#60;PhysicsComponent#62;#40;#41; → true"]
        Pos["pos#60;RenderComponent#62;#40;#41; → 1"]
    end
    
    subgraph "Type Extraction"
        Nth0["nth_type_t#60;0, Types...#62; → PhysicsComponent"]
        Nth1["nth_type_t#60;1, Types...#62; → RenderComponent"]
        Nth2["nth_type_t#60;2, Types...#62; → LightComponent"]
    end
    
    TL --> Size
    TL --> Contains
    TL --> Pos
    TL --> Nth0
    TL --> Nth1
    TL --> Nth2
    
    style TL fill:#E1F5FE
    style Size fill:#C8E6C9
    style Contains fill:#C8E6C9
    style Pos fill:#C8E6C9
Loading

Core Utilities

nth_type - Extract Type at Position

Gets the Nth type from a parameter pack:

template <std::size_t N, typename... TYPES>
struct nth_type;

template <std::size_t N, typename... TYPES>
using nth_type_t = typename nth_type<N, TYPES...>::type;

Implementation (recursive template specialization):

// Base case: N=0, return first type
template <typename T, typename... TYPES>
struct nth_type<0, T, TYPES...> : type_id<T> {};

// Recursive case: decrement N, skip first type
template <std::size_t N, typename T, typename... TYPES>
struct nth_type<N, T, TYPES...> : type_id<nth_type_t<N-1, TYPES...>> {};

Example:

using Types = Typelist<int, float, double, char>;

static_assert(std::is_same_v<nth_type_t<0, int, float, double>, int>);
static_assert(std::is_same_v<nth_type_t<1, int, float, double>, float>);
static_assert(std::is_same_v<nth_type_t<2, int, float, double>, double>);

Compilation Trace for nth_type_t<2, int, float, double>:

nth_type<2, int, float, double>
  → nth_type<1, float, double>  (skip int)
    → nth_type<0, double>       (skip float)
      → type_id<double>          (base case)
        → double

pos_type - Find Type Position

Returns the index of a type in a parameter pack:

template <typename T, typename... TYPES>
struct pos_type;

template <typename T, typename... TYPES>
constexpr std::size_t pos_type_v = pos_type<T, TYPES...>::value;

Implementation:

// Base case: found type at position 0
template <typename T, typename... TYPES>
struct pos_type<T, T, TYPES...> : constant<std::size_t, 0> {};

// Recursive case: not found, increment position
template <typename T, typename U, typename... TYPES>
struct pos_type<T, U, TYPES...> : constant<std::size_t, 1 + pos_type_v<T, TYPES...>> {};

Example:

static_assert(pos_type_v<float, int, float, double> == 1);
static_assert(pos_type_v<double, int, float, double> == 2);

templateif_t - Compile-time Conditional

Selects between two types based on a boolean constant:

template <bool CONDITION, typename T, typename F>
using templateif_t = typename IFT<CONDITION, T, F>::type;

// Implementation
template <bool CONDITION, typename T, typename F>
struct IFT : type_id<F> {};  // Default: false branch

template <typename T, typename F>
struct IFT<true, T, F> : type_id<T> {};  // Specialization: true branch

Example:

using Type1 = templateif_t<true, int, float>;   // → int
using Type2 = templateif_t<false, int, float>;  // → float

// Practical use: select container based on size
template <std::size_t N>
using SmallOrLargeVector = templateif_t<
    (N < 100),
    std::array<int, N>,     // Small: use array
    std::vector<int>        // Large: use vector
>;

Type Transformations

replace_t - Change Template Wrapper

Transforms Typelist<A, B, C> into NewTemplate<A, B, C>:

template <template <typename...> class N, typename L>
struct replace;

template <template <typename...> class N, typename... TYPES>
struct replace<N, Typelist<TYPES...>> : type_id<N<TYPES...>> {};

template <template <typename...> class N, typename L>
using replace_t = typename replace<N, L>::type;

Example:

using CompList = Typelist<int, float, double>;

using Tuple = replace_t<std::tuple, CompList>;
// Result: std::tuple<int, float, double>

using Variant = replace_t<std::variant, CompList>;
// Result: std::variant<int, float, double>

Visualization:

graph LR
    Input["Typelist#60;A, B, C#62;"] --> Replace["replace_t#60;std::tuple, ...#62;"]
    Replace --> Output["std::tuple#60;A, B, C#62;"]
    
    Input2["Typelist#60;A, B, C#62;"] --> Replace2["replace_t#60;std::variant, ...#62;"]
    Replace2 --> Output2["std::variant#60;A, B, C#62;"]
    
    style Input fill:#E1F5FE
    style Input2 fill:#E1F5FE
    style Output fill:#C8E6C9
    style Output2 fill:#C8E6C9
Loading

mp_transform - Map Function Over Types

Applies a template transformation to each type in a list:

template <template<class...> class F, class L>
struct mp_transform_impl;

template <template<class...> class F, template<class...> class L, class... T>
struct mp_transform_impl<F, L<T...>> : type_id<L<F<T>...>> {};

template <template<class...> class F, class L>
using mp_transform = typename mp_transform_impl<F, L>::type;

Example:

template <typename T>
using AddPointer = T*;

using Input = Typelist<int, float, double>;
using Output = mp_transform<AddPointer, Input>;
// Result: Typelist<int*, float*, double*>

Real-World ECS Use:

// Transform component types to slotmap types
template <typename T>
using to_slotmap = Slotmap<T, 1024>;

using Components = Typelist<PhysicsComponent, RenderComponent>;
using Slotmaps = mp_transform<to_slotmap, Components>;
// Result: Typelist<Slotmap<PhysicsComponent, 1024>, Slotmap<RenderComponent, 1024>>

// Convert to tuple for storage
using StorageTuple = replace_t<std::tuple, Slotmaps>;
// Result: std::tuple<Slotmap<PhysicsComponent, 1024>, Slotmap<RenderComponent, 1024>>

Transformation Pipeline:

graph TD
    Input["Typelist#60;PhysicsComponent, RenderComponent#62;"]
    
    Transform["mp_transform#60;to_slotmap, ...#62;"]
    
    Intermediate["Typelist#60;Slotmap#60;Physics#62;, Slotmap#60;Render#62;#62;"]
    
    Replace["replace_t#60;std::tuple, ...#62;"]
    
    Output["std::tuple#60;Slotmap#60;Physics#62;, Slotmap#60;Render#62;#62;"]
    
    Input --> Transform
    Transform --> Intermediate
    Intermediate --> Replace
    Replace --> Output
    
    style Input fill:#E1F5FE
    style Intermediate fill:#FFF9C4
    style Output fill:#C8E6C9
Loading

Component Traits System

Automatic Mask Type Selection

The engine automatically selects the smallest integer type that can hold component masks:

template <typename LIST>
using select_smallest_mask_type_t =
    templateif_t<(LIST::size() <= 8), uint8_t,
    templateif_t<(LIST::size() <= 16), uint16_t,
    templateif_t<(LIST::size() <= 32), uint32_t,
    uint64_t>>>;

Selection Logic:

graph TD
    Start["Component Count"] --> Check8{≤ 8 components?}
    Check8 -->|Yes| UseU8["uint8_t #40;1 byte#41;"]
    Check8 -->|No| Check16{≤ 16 components?}
    
    Check16 -->|Yes| UseU16["uint16_t #40;2 bytes#41;"]
    Check16 -->|No| Check32{≤ 32 components?}
    
    Check32 -->|Yes| UseU32["uint32_t #40;4 bytes#41;"]
    Check32 -->|No| UseU64["uint64_t #40;8 bytes#41;"]
    
    UseU8 --> End[Mask Type Selected]
    UseU16 --> End
    UseU32 --> End
    UseU64 --> End
    
    style UseU8 fill:#C8E6C9
    style UseU16 fill:#FFF9C4
    style UseU32 fill:#FFCCBC
    style UseU64 fill:#FFCDD2
Loading

Example:

using SmallList = Typelist<A, B, C>;  // 3 components
using SmallMask = select_smallest_mask_type_t<SmallList>;  // → uint8_t

using LargeList = Typelist<C1, C2, ..., C20>;  // 20 components
using LargeMask = select_smallest_mask_type_t<LargeList>;  // → uint32_t

common_traits - Unified Component/Tag Info

Provides compile-time information about typelists:

template <typename LIST>
struct common_traits {
    static_assert(LIST::size() <= 64, "Maximum of 64 types");
    
    using mask_type = select_smallest_mask_type_t<LIST>;
    
    consteval static uint8_t size() noexcept {
        return LIST::size();
    }
    
    template <typename ITEM>
    consteval static uint8_t id() noexcept {
        static_assert(LIST::template contains<ITEM>());
        return LIST::template pos<ITEM>();
    }
    
    template <typename... ITEMS>
    consteval static mask_type mask() noexcept {
        return (0 | ... | (1 << id<ITEMS>()));
    }
};

Mask Generation Example:

using Components = Typelist<Physics, Render, Light>;
using Traits = common_traits<Components>;

// Single component mask
constexpr auto physics_mask = Traits::mask<Physics>();  // 0b001
constexpr auto render_mask = Traits::mask<Render>();    // 0b010

// Multiple components mask
constexpr auto both_mask = Traits::mask<Physics, Render>();  // 0b011

// Fold expression expands to:
// 0 | (1 << 0) | (1 << 1)
// = 0b011

Mask Generation Diagram:

graph TB
    subgraph "Input: Components"
        C1["Physics #40;id=0#41;"]
        C2["Render #40;id=1#41;"]
        C3["Light #40;id=2#41;"]
    end
    
    subgraph "Mask Generation"
        M1["1 << 0 = 0b001"]
        M2["1 << 1 = 0b010"]
        M3["1 << 2 = 0b100"]
    end
    
    subgraph "Combined Mask"
        Combined["0b001 | 0b010 | 0b100<br/>=<br/>0b111"]
    end
    
    C1 --> M1
    C2 --> M2
    C3 --> M3
    
    M1 --> Combined
    M2 --> Combined
    M3 --> Combined
    
    style Combined fill:#C8E6C9
Loading

Specialized Traits

template <typename TAGS>
struct tag_traits : common_traits<TAGS> {};

template <typename COMPONENTS>
struct component_traits : tag_traits<COMPONENTS> {};

These provide semantic aliases for clarity:

  • component_traits<ComponentList> - For regular components
  • tag_traits<TagList> - For zero-size markers

Practical Applications

Building Component Storage

The ComponentStorage uses template metaprogramming extensively:

template <typename COMPONENT_LIST, typename SINGLETON_LIST, typename TAG_LIST, std::size_t Capacity>
struct ComponentStorage {
    // Transform component types to slotmaps
    template <typename T>
    using to_slotmap = Slotmap<T, Capacity>;
    
    // Apply transformation
    using slotmap_list = mp_transform<to_slotmap, COMPONENT_LIST>;
    
    // Convert to tuple
    template <typename T>
    using to_tuple = replace_t<std::tuple, T>;
    
    using storage_type = to_tuple<slotmap_list>;
    // Result: std::tuple<Slotmap<C1, Cap>, Slotmap<C2, Cap>, ...>
    
    // Singleton components stored directly
    using storage_singleton_type = to_tuple<SINGLETON_LIST>;
    // Result: std::tuple<S1, S2, ...>
    
private:
    storage_type m_component_tuple{};
    storage_singleton_type m_singleton_component_tuple{};
};

Step-by-Step Transformation:

Stage Type
Input Typelist<Physics, Render>
After mp_transform Typelist<Slotmap<Physics, 1024>, Slotmap<Render, 1024>>
After replace_t std::tuple<Slotmap<Physics, 1024>, Slotmap<Render, 1024>>

Type-Safe Component Access

template <typename COMPONENT>
constexpr auto& get_storage() noexcept {
    // Get compile-time index of component
    constexpr auto id = component_info::template id<COMPONENT>();
    
    // Access tuple element at that index
    return std::get<id>(m_component_tuple);
}

Compile-Time Resolution:

auto& storage = component_storage.get_storage<PhysicsComponent>();

// Compiler expands to:
constexpr auto id = component_traits<ComponentList>::id<PhysicsComponent>();  // → 0
return std::get<0>(m_component_tuple);  // Direct tuple access, no runtime lookup!

Entity Key Storage

Entities store keys for each component type:

struct Entity {
    // Transform component types to key types
    template <typename T>
    using to_key_type = typename Slotmap<T, CAPACITY>::key_type;
    
    using key_type_list = mp_transform<to_key_type, COMPONENT_LIST>;
    // Result: Typelist<key_type<C1>, key_type<C2>, ...>
    
    using key_storage_t = replace_t<std::tuple, key_type_list>;
    // Result: std::tuple<key_type<C1>, key_type<C2>, ...>
    
private:
    key_storage_t m_component_keys{};
};

For 4 components:

using key_storage_t = std::tuple<
    Slotmap<PhysicsComponent>::key_type,    // {uint32_t id, uint32_t gen}
    Slotmap<RenderComponent>::key_type,
    Slotmap<LightComponent>::key_type,
    Slotmap<ShadowComponent>::key_type
>;
// Total: 4 * 8 bytes = 32 bytes per entity

Compilation Process

Example: Creating EntityManager

using Components = Typelist<PhysicsComponent, RenderComponent>;
using Singletons = Typelist<CameraComponent>;
using EntityManager = ADE::EntityManager<Components, Singletons, Typelist<>, 1024>;

Compilation Flow:

graph TD
    Start["EntityManager#60;Components, Singletons, Tags, 1024#62;"] --> CS["Instantiate ComponentStorage"]
    
    CS --> Transform1["Transform Components → Slotmaps"]
    Transform1 --> Tuple1["Create storage_type = std::tuple#60;Slotmaps...#62;"]
    
    CS --> Transform2["Transform Singletons → Direct types"]
    Transform2 --> Tuple2["Create singleton_storage = std::tuple#60;Singletons...#62;"]
    
    Start --> Entity["Instantiate Entity"]
    Entity --> KeyTransform["Transform Components → Key types"]
    KeyTransform --> KeyTuple["Create key_storage_t = std::tuple#60;Keys...#62;"]
    
    Start --> Traits["Instantiate component_traits"]
    Traits --> MaskType["Select mask_type based on size"]
    Traits --> GenMasks["Generate mask#60;#62; functions"]
    
    Tuple1 --> AllocMem["Allocate memory for storage"]
    Tuple2 --> AllocMem
    KeyTuple --> AllocEntity["Allocate Entity struct"]
    MaskType --> AllocEntity
    
    AllocMem --> Final["EntityManager ready"]
    AllocEntity --> Final
    GenMasks --> Final
    
    style Start fill:#E1F5FE
    style Final fill:#C8E6C9
Loading

All computations happen at compile-time! The runtime code sees only:

  • Concrete structs with fixed sizes
  • Direct array/tuple accesses
  • No type erasure, no virtual functions

Compiler Output (Conceptual)

For EntityManager<Typelist<PhysicsComponent, RenderComponent>, ...>:

// What the compiler generates (simplified):
struct EntityManager {
    struct Entity {
        uint16_t m_component_mask;  // 2 components → uint16_t
        std::tuple<
            key_type<PhysicsComponent>,
            key_type<RenderComponent>
        > m_component_keys;
    };
    
    std::tuple<
        Slotmap<PhysicsComponent, 1024>,
        Slotmap<RenderComponent, 1024>
    > m_components;
    
    // get_storage<PhysicsComponent>() compiled to:
    auto& get_physics_storage() {
        return std::get<0>(m_components);  // Hardcoded index!
    }
};

Performance Impact

Zero Runtime Overhead

Traditional Approach (runtime polymorphism):

Component* component = entity.get_component("PhysicsComponent");  // String lookup
PhysicsComponent* physics = dynamic_cast<PhysicsComponent*>(component);  // RTTI
if (physics) {
    physics->update();  // Virtual function call
}

Template Metaprogramming Approach:

auto& physics = entity_manager.get_component<PhysicsComponent>(entity);
// Compiled to:
// 1. constexpr id = 0
// 2. std::get<0>(tuple)[entity_key.id]
// Direct memory access, no lookups, no virtuals!

Compile-Time Guarantees

// Compile error: component not in typelist
entity_manager.add_component<UnknownComponent>(entity);
// Error: static_assert failed in component_info::id<UnknownComponent>()

// Compile error: mismatched foreach types
entity_manager.foreach<
    Typelist<PhysicsComponent>,
    Typelist<>
>([](Entity& e, RenderComponent& r) {  // Wrong component type!
    // Error: cannot convert Typelist<PhysicsComponent> to RenderComponent&
});

Advanced Patterns

Conditional Component Storage

template <typename COMPONENT>
using storage_for = templateif_t<
    std::is_trivially_copyable_v<COMPONENT>,
    std::array<COMPONENT, 1024>,   // POD: use array
    std::vector<COMPONENT>         // Non-POD: use vector
>;

Type-Level Filtering

// Filter components by size
template <typename T>
using is_small = std::bool_constant<sizeof(T) <= 16>;

// Get only small components from list (hypothetical)
using SmallComponents = filter_if<ComponentList, is_small>;

Related Documentation


Next: Explore Component System to see templates in action.