Skip to content
C++
Idiom
since C++11
Advanced

TypeList — Compile-Time Type Lists

"TypeList idiom: manipulate ordered sets of types at compile time with head/tail decomposition, filter, transform, and runtime dispatch patterns."

TypeListsince C++11

A TypeList is a variadic template struct used as a compile-time container of types, enabling recursive type-level algorithms that run entirely at compile time with zero runtime cost.

Overview

Variadic templates (C++11) let you bundle an arbitrary sequence of types into a single struct. That struct — the TypeList — serves as the input and output of compile-time algorithms: slicing, filtering, transforming, searching, and dispatching to runtime code once per type.

TypeLists appear in ECS (entity-component systems) to register component types, in serialization frameworks to enumerate codec targets, in reflection stubs to iterate fields, and in plugin architectures to wire handlers to message types.

The fundamental operations build on one rule: strip the head, recurse on the tail, reassemble on the way back. C++17 fold expressions flatten the need for recursion in the dispatch-only case but do not eliminate it for structural operations that return new TypeLists.

Syntax

cpp
// The list itself — just a tag struct carrying types as template arguments
template<typename... Ts>
struct TypeList {};                          // C++11

// Alias for an empty list
using Empty = TypeList<>;

All operations are partial specialisations on TypeList<T, Ts...> that peel the head type T off and recurse over Ts....

Core Operations

cpp
// Size
template<typename List>              struct size;
template<typename... Ts>
struct size<TypeList<Ts...>>
    : std::integral_constant<std::size_t, sizeof...(Ts)> {};  // C++11

// Head / tail
template<typename List>              struct head;
template<typename T, typename... Ts>
struct head<TypeList<T, Ts...>>      { using type = T; };

template<typename List>              struct tail;
template<typename T, typename... Ts>
struct tail<TypeList<T, Ts...>>      { using type = TypeList<Ts...>; };

// Nth element — O(N) template instantiations
template<std::size_t I, typename List>    struct nth;
template<typename T, typename... Ts>
struct nth<0, TypeList<T, Ts...>>         { using type = T; };
template<std::size_t I, typename T, typename... Ts>
struct nth<I, TypeList<T, Ts...>>
    : nth<I - 1, TypeList<Ts...>> {};     // recursive peel

// C++26 pack indexing eliminates this entirely: Ts...[I]

// Prepend / append
template<typename T, typename List>    struct prepend;
template<typename T, typename... Ts>
struct prepend<T, TypeList<Ts...>>     { using type = TypeList<T, Ts...>; };

template<typename T, typename List>    struct append;
template<typename T, typename... Ts>
struct append<T, TypeList<Ts...>>      { using type = TypeList<Ts..., T>; };

// Concatenate two lists
template<typename A, typename B>       struct concat;
template<typename... As, typename... Bs>
struct concat<TypeList<As...>, TypeList<Bs...>>
    { using type = TypeList<As..., Bs...>; };

Filter

cpp
template<template<typename> class Pred, typename List>
struct filter { using type = TypeList<>; };

template<template<typename> class Pred, typename T, typename... Ts>
struct filter<Pred, TypeList<T, Ts...>> {
    using rest = typename filter<Pred, TypeList<Ts...>>::type;
    using type = std::conditional_t<                   // C++14
        Pred<T>::value,
        typename prepend<T, rest>::type,
        rest>;
};

// Example: keep only integral types
using Mixed    = TypeList<int, float, char, double, bool, unsigned>;
using Integers = filter<std::is_integral, Mixed>::type;
// TypeList<int, char, bool, unsigned>

Transform

cpp
template<template<typename> class F, typename List>
struct transform { using type = TypeList<>; };

template<template<typename> class F, typename T, typename... Ts>
struct transform<F, TypeList<T, Ts...>> {
    using type = typename prepend<
        typename F<T>::type,
        typename transform<F, TypeList<Ts...>>::type
    >::type;
};

// Make every type a pointer
template<typename T> struct add_ptr { using type = T*; };
using PtrTypes = transform<add_ptr, Integers>::type;
// TypeList<int*, char*, bool*, unsigned*>

Examples

Runtime Dispatch: for_each_type

The most common use of a TypeList is iterating over its types at runtime — calling a generic lambda once per type:

cpp
// C++20 template lambda syntax
template<typename... Ts, typename F>
void for_each_type(TypeList<Ts...>, F&& f) {
    (f.template operator()<Ts>(), ...);  // C++17 fold expression
}

// Print sizeof every type in a list
using Scalars = TypeList<char, short, int, long, float, double>;
for_each_type(Scalars{}, []<typename T>() {             // C++20 lambda template
    std::println("{:<10} {} bytes", typeid(T).name(), sizeof(T));
});

For pre-C++20 compilers without template lambdas, wrap the per-type logic in a struct:

cpp
// C++17 alternative
struct PrintSize {
    template<typename T>
    void operator()() const {
        std::printf("%-10s %zu bytes\n", typeid(T).name(), sizeof(T));
    }
};
for_each_type(Scalars{}, PrintSize{});

Generating a std::variant from a TypeList

cpp
template<typename List>         struct to_variant;
template<typename... Ts>
struct to_variant<TypeList<Ts...>> { using type = std::variant<Ts...>; };  // C++17

using MessageTypes = TypeList<MoveMsg, AttackMsg, SpawnMsg, DespawnMsg>;
using MessageVariant = to_variant<MessageTypes>::type;
// std::variant<MoveMsg, AttackMsg, SpawnMsg, DespawnMsg>

This pattern replaces hand-written std::variant declarations that fall out of sync when the type list grows.

ECS Component Registration

cpp
using Components = TypeList<Transform, Velocity, Health, Sprite>;

class World {
public:
    void register_all() {
        for_each_type(Components{}, [this]<typename C>() {
            storage_.emplace(
                std::type_index(typeid(C)),
                std::make_unique<ComponentStorage<C>>()
            );
        });
    }
private:
    std::unordered_map<std::type_index,
                       std::unique_ptr<IStorage>> storage_;
};

One change to Components propagates to registration, serialisation, and any other loop that iterates it.

Index-Based Dispatch

When you need both the type and its index within the list, pair for_each_type with std::index_sequence:

cpp
template<typename... Ts, typename F, std::size_t... Is>
void for_each_type_indexed_impl(TypeList<Ts...>, F&& f,
                                std::index_sequence<Is...>) {  // C++14
    (f.template operator()<Ts, Is>(), ...);
}

template<typename... Ts, typename F>
void for_each_type_indexed(TypeList<Ts...> list, F&& f) {
    for_each_type_indexed_impl(list, std::forward<F>(f),
                               std::index_sequence_for<Ts...>{});
}

// Assign a stable numeric tag to each component type
for_each_type_indexed(Components{}, []<typename C, std::size_t I>() {
    ComponentRegistry::set_id<C>(I);
});

Best Practices

Prefer flat pack expansion when you only need dispatch. If you never restructure the list — never filter or transform it — a bare parameter pack in a function template is simpler and compiles faster:

cpp
template<typename... Ts>
void register_all(Registry& r) {
    (r.add<Ts>(), ...);  // C++17 fold — no TypeList needed
}
register_all<int, float, std::string>(registry);

Introduce TypeList only when you need to store a named type set, pass it between translation units as a type alias, or apply structural operations like filter/transform.

Use _t and _v helpers to reduce verbosity. Every operation that yields a type should expose a _t alias; predicates should expose a _v constexpr bool:

cpp
template<typename T, typename List>
inline constexpr bool contains_v =
    contains<T, List>::value;  // C++17 inline variable

template<template<typename> class Pred, typename List>
using filter_t = typename filter<Pred, List>::type;

Keep list depth below ~100 types. Each nested partial specialisation is an independent template instantiation. Deep lists generate significant symbol table bloat and slow incremental builds. For very large sets, consider splitting into sub-lists and concatenating.

Common Pitfalls

Ambiguous base-class instantiation with index 0. The nth<0, ...> specialisation must appear before the recursive case. If the recursive case is listed first, compilers that do not respect declaration order for specialisations may instantiate it with I = 0 and recurse infinitely.

Forgetting typename on dependent types. Every ::type inside a template context requires typename. Omitting it produces a hard-to-read error:

cpp
// Wrong:  filter<Pred, Tail>::type
// Right:  typename filter<Pred, Tail>::type

Empty list instantiation. Operations like head and tail have no valid specialisation for TypeList<>. Calling them on an empty list is a hard error. Guard recursive algorithms:

cpp
template<typename List>
struct head;  // primary intentionally undefined — compile error on TypeList<>

ODR exposure through explicit instantiation. If you explicitly instantiate a TypeList alias in a header, every translation unit that includes the header re-instantiates the full dependency graph. Prefer extern template or keep the aliases in a single .cpp.

See Also

  • CRTP — another compile-time pattern that encodes type relationships in the template system
  • Policy-Based Design — composes behaviour from type parameters rather than type lists
  • Type Erasure — the runtime complement: erase a bounded set of types behind a uniform interface
  • Templates — variadic templates and pack expansions that underpin TypeList