-
Notifications
You must be signed in to change notification settings - Fork 0
Template Metaprogramming
- Overview
- Typelist Fundamentals
- Core Utilities
- Type Transformations
- Component Traits System
- Practical Applications
- Compilation Process
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
✅ 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
A Typelist is a template container that holds types instead of values:
namespace META_TYPES {
template <typename... TYPES>
struct Typelist {
// ... compile-time operations ...
};
}// 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);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
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
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);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 branchExample:
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
>;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
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
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
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_tProvides 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)
// = 0b011Mask 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
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
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>> |
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!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 entityusing 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
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
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!
}
};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 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&
});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
>;// 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>;- Entity-Component-System - How templates power the ECS
- Slotmap Implementation - Template-based container
- Component System - Template-driven storage
- Usage Guide - Practical examples
Next: Explore Component System to see templates in action.