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++11A 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
// 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
// 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
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
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:
// 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:
// 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
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
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:
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:
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:
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:
// Wrong: filter<Pred, Tail>::type
// Right: typename filter<Pred, Tail>::typeEmpty 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:
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.
Type List Operations: Append, Prepend, Concat
A TypeList's structural operations are pure type-level functions: they consume one or more lists and produce a new list without touching runtime memory. Append, prepend, and concat cover the three fundamental ways to combine or extend a list.
// C++11
// Append: TypeList<A, B> + C → TypeList<A, B, C>
template<typename List, typename T> struct Append;
template<typename... Ts, typename T>
struct Append<TypeList<Ts...>, T> {
using type = TypeList<Ts..., T>;
};
// Prepend: C + TypeList<A, B> → TypeList<C, A, B>
template<typename T, typename List> struct Prepend;
template<typename T, typename... Ts>
struct Prepend<T, TypeList<Ts...>> {
using type = TypeList<T, Ts...>;
};
// Concat: TypeList<A, B> + TypeList<C, D> → TypeList<A, B, C, D>
template<typename ListA, typename ListB> struct Concat;
template<typename... As, typename... Bs>
struct Concat<TypeList<As...>, TypeList<Bs...>> {
using type = TypeList<As..., Bs...>; // single pack expansion, no recursion
};
// Convenience aliases
template<typename List, typename T>
using Append_t = typename Append<List, T>::type;
template<typename T, typename List>
using Prepend_t = typename Prepend<T, List>::type;
template<typename ListA, typename ListB>
using Concat_t = typename Concat<ListA, ListB>::type;Pack expansion in the Concat specialisation does the heavy lifting in a single template instantiation, making it far cheaper at compile time than a recursive approach. Append and Prepend are symmetric single-pack expansions that also avoid recursion entirely.
// C++11 — usage
using AB = TypeList<int, float>;
using ABC = Append_t<AB, double>; // TypeList<int, float, double>
using CAB = Prepend_t<char, AB>; // TypeList<char, int, float>
using ABCD = Concat_t<AB, TypeList<bool, short>>;
// TypeList<int, float, bool, short>Type List Algorithms: Filter and Transform
Filter and Transform are the two workhorses of type-level functional programming: Filter selects types by a predicate, Transform rewrites every type through a meta-function. Both follow the same recursive head/tail skeleton that terminates when the list is empty.
// C++11 recursive implementations
// Filter<Pred, TypeList<Ts...>>: keep types where Pred<T>::value is true
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<
Pred<T>::value,
typename Prepend<T, Rest>::type,
Rest>;
};
// Transform<F, TypeList<Ts...>>: apply F<T>::type to each element
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;
};C++17 fold expressions do not help with structural filter/transform (which must produce a new TypeList), but they do replace the recursion in pure-dispatch loops. The recursive implementations above are still the idiomatic solution for structural operations in C++11 through C++23.
// C++11 — filter example
using Mixed = TypeList<int, double, char, float, bool>;
using Integers = typename Filter<std::is_integral, Mixed>::type;
// TypeList<int, char, bool>
// C++11 — transform example
template<typename T> struct AddConst { using type = const T; };
using ConstInts = typename Transform<AddConst, Integers>::type;
// TypeList<const int, const char, const bool>
// C++17 — fold-expression dispatch (does NOT produce a TypeList, but avoids
// recursion when only iteration is needed):
template<template<typename> class Pred, typename... Ts>
constexpr std::size_t count_if(TypeList<Ts...>) {
return (std::size_t{0} + ... + (Pred<Ts>::value ? 1u : 0u));
}
static_assert(count_if<std::is_integral>(Mixed{}) == 3);Type List Index Operations: At, IndexOf, Size
Random access into a TypeList at compile time — retrieving the Nth type, locating a type's index, or measuring the list's length — are the compile-time equivalents of operator[], std::find, and .size(). All three are constant expressions with no runtime cost.
// C++11
// Size<TypeList<Ts...>> — number of types in the list
template<typename List> struct Size;
template<typename... Ts>
struct Size<TypeList<Ts...>>
: std::integral_constant<std::size_t, sizeof...(Ts)> {};
template<typename List>
inline constexpr std::size_t Size_v = Size<List>::value; // C++17
// At<N, TypeList<Ts...>> — Nth type via std::tuple_element_t (avoids manual recursion)
template<std::size_t N, typename List> struct At;
template<std::size_t N, typename... Ts>
struct At<N, TypeList<Ts...>> {
using type = std::tuple_element_t<N, std::tuple<Ts...>>; // delegates to stdlib
};
template<std::size_t N, typename List>
using At_t = typename At<N, List>::type;
// IndexOf<T, TypeList<Ts...>> — first index of T, compile error if absent
template<typename T, typename List, std::size_t I = 0> struct IndexOf;
template<typename T, typename... Ts, std::size_t I>
struct IndexOf<T, TypeList<T, Ts...>, I>
: std::integral_constant<std::size_t, I> {}; // found at position I
template<typename T, typename U, typename... Ts, std::size_t I>
struct IndexOf<T, TypeList<U, Ts...>, I>
: IndexOf<T, TypeList<Ts...>, I + 1> {}; // recurse with incremented index
template<typename T, typename List>
inline constexpr std::size_t IndexOf_v = IndexOf<T, List>::value; // C++17At deliberately delegates to std::tuple_element_t rather than reimplementing recursion: the standard library implementation is already compiler-optimised (many vendors special-case it with __type_pack_element). IndexOf uses a defaulted non-type parameter I to carry the running index without a helper struct.
// C++11 — usage
using List = TypeList<char, short, int, long, float, double>;
static_assert(Size_v<List> == 6);
static_assert(std::is_same_v<At_t<2, List>, int>);
static_assert(IndexOf_v<float, List> == 4);
// Runtime complement: std::get<N> on a tuple of the same types
auto values = std::tuple<char, short, int, long, float, double>{};
auto third = std::get<IndexOf_v<int, List>>(values); // runtime lookup driven by compile-time indexType Lists with std::variant (C++17)
Converting a TypeList to a std::variant bridges the compile-time type catalogue to a runtime value that can hold any one of those types. This pattern is the backbone of type-safe message buses, event systems, and plugin registries.
// C++17
template<typename List> struct ToVariant;
template<typename... Ts>
struct ToVariant<TypeList<Ts...>> { using type = std::variant<Ts...>; };
template<typename List>
using ToVariant_t = typename ToVariant<List>::type;
// Define all message types in one canonical list
using MessageTypes = TypeList<MoveMsg, AttackMsg, SpawnMsg, DespawnMsg>;
using MessageVariant = ToVariant_t<MessageTypes>;
// std::variant<MoveMsg, AttackMsg, SpawnMsg, DespawnMsg>Because the variant's parameter list is derived from the same MessageTypes alias used everywhere else, adding a new message type to MessageTypes automatically expands the variant — no manual synchronisation needed.
// C++17 — std::visit dispatch on the resulting variant
void handle(const MessageVariant& msg) {
std::visit([](const auto& m) {
using T = std::decay_t<decltype(m)>;
if constexpr (std::is_same_v<T, MoveMsg>)
process_move(m);
else if constexpr (std::is_same_v<T, AttackMsg>)
process_attack(m);
else if constexpr (std::is_same_v<T, SpawnMsg>)
process_spawn(m);
else if constexpr (std::is_same_v<T, DespawnMsg>)
process_despawn(m);
}, msg);
}
// Or use an overloaded visitor built from the same TypeList:
template<typename... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
template<typename... Ts> Overloaded(Ts...) -> Overloaded<Ts...>; // deduction guideType Lists and Reflection (C++26 Preview)
P2996 static reflection makes many hand-rolled TypeList patterns unnecessary in C++26: the compiler can enumerate members, bases, and enumerators natively. Understanding both approaches is essential — TypeLists remain the right tool for pre-C++26 codebases and for operations that operate on user-defined type sets rather than struct members.
// C++26 — P2996 static reflection replaces a manually-built TypeList of member types
// Pre-C++26: you would build a TypeList<int, float, std::string> by hand
// and iterate it to serialise each field.
// C++26: let the compiler build the member list
template<typename T>
void serialize_all_fields(const T& obj) {
// std::meta::nonstatic_data_members_of returns a compile-time range
// of std::meta::info values — no TypeList needed
[:std::meta::nonstatic_data_members_of(^^T):] // splice each member
// 'template for' replaces recursive TypeList iteration:
template for (constexpr auto mem : std::meta::nonstatic_data_members_of(^^T)) {
serialize_field(obj.[:mem:]);
}
}template for (P1306) iterates a compile-time range directly, replacing the for_each_type pattern that drives TypeList dispatch. However, reflection only covers things the compiler knows about — class members, bases, enumerators. A TypeList of arbitrary user-chosen types (e.g., a plugin registry, an ECS component set) is still a manual declaration that reflection cannot derive.
// C++26 — TypeLists still needed for user-defined type sets
// Reflection tells you about MoveMsg's fields; it cannot tell you
// which message types your application registered:
using MessageTypes = TypeList<MoveMsg, AttackMsg, SpawnMsg>; // still explicitTypeList idioms therefore remain valuable for (1) pre-C++26 targets, (2) cross-cutting type sets that are not members of any single struct, and (3) operations such as filter and transform that construct new type sequences not representable as a reflected entity.
Practical Type List: Dispatcher Table
A compile-time dispatcher table maps a runtime type identifier to a pre-registered handler, giving O(1) dispatch without virtual functions or dynamic_cast. The table is a std::array sized and populated at compile time using std::index_sequence.
// C++14
#include <array>
#include <cstddef>
#include <functional>
#include <utility>
// Generic message base used only to carry a type tag at runtime
struct IMessage { virtual ~IMessage() = default; };
template<typename List>
class Dispatcher;
template<typename... Ts>
class Dispatcher<TypeList<Ts...>> {
public:
using Handler = std::function<void(const IMessage&)>;
static constexpr std::size_t N = sizeof...(Ts);
// Register a handler for type T; T must be in the TypeList
template<typename T>
void set(Handler h) {
constexpr std::size_t idx = IndexOf_v<T, TypeList<Ts...>>;
table_[idx] = std::move(h);
}
// Dispatch to the handler at slot 'type_id' (runtime value)
void call(std::size_t type_id, const IMessage& msg) const {
if (type_id < N && table_[type_id])
table_[type_id](msg);
}
private:
std::array<Handler, N> table_{};
};The std::array<Handler, N> is stack-allocated and cache-friendly. IndexOf_v converts the type to an index at compile time, so set<T>() writes directly to the correct slot with no runtime search.
// C++14 — usage: message handler registration
struct MoveMsg : IMessage { int dx, dy; };
struct AttackMsg: IMessage { int damage; };
struct SpawnMsg : IMessage { int entity_id; };
using MsgTypes = TypeList<MoveMsg, AttackMsg, SpawnMsg>;
Dispatcher<MsgTypes> dispatcher;
dispatcher.set<MoveMsg>([](const IMessage& m) {
const auto& mv = static_cast<const MoveMsg&>(m);
apply_move(mv.dx, mv.dy);
});
dispatcher.set<AttackMsg>([](const IMessage& m) {
const auto& atk = static_cast<const AttackMsg&>(m);
apply_damage(atk.damage);
});
// Runtime dispatch — O(1) array lookup
dispatcher.call(IndexOf_v<MoveMsg, MsgTypes>, some_message);For index-sequence-driven table initialisation (useful when default-constructing handlers from the TypeList itself), pass std::index_sequence_for<Ts...>{} to a helper and expand with a fold expression over the indices.
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