template for
Compile-time expansion statement (C++26) that iterates over a parameter pack, instantiating the loop body once per element without recursive template machinery.
template forsince C++26An expansion statement that iterates over a parameter pack or tuple-like type at compile time, instantiating the loop body once for each element without recursive template specialisations or index-sequence boilerplate.
Overview
Before C++26, iterating over a heterogeneous sequence at compile time required one of three approaches: recursive template specialisations (verbose, slow to compile), fold expressions (limited to single expressions), or std::apply + std::index_sequence tricks (clever but opaque). The template for expansion statement β standardised in C++26 via P1306 β provides the same readable, intent-revealing loop syntax as range-based for, but at the type-system level.
The critical difference from a runtime loop: each iteration is a distinct template instantiation. The loop variable's type can differ between iterations. This is why if constexpr, requires, and type-dependent operations work naturally inside the body, and why there is no runtime overhead.
// C++17 β fold expression: concise but limited to single-expression bodies
template <typename... Ts>
std::size_t total_sizeof_fold() {
return (sizeof(Ts) + ... + 0); // C++17
}
// C++26 β expansion statement: full statement body, locally readable
template <typename... Ts>
std::size_t total_sizeof() {
std::size_t n = 0;
template for (auto t : std::tuple<std::type_identity<Ts>...>{}) { // C++26
using T = typename decltype(t)::type;
n += sizeof(T);
}
return n;
}
static_assert(total_sizeof<char, int, double>() == 1 + 4 + 8);Syntax
template for ( for-range-declaration : for-range-initializer ) statementThe for-range-initializer must be a compile-time expandable entity: a parameter pack, a tuple-like type whose std::tuple_size is known at compile time, or a constexpr range with a compile-time element count. The for-range-declaration follows the same rules as range-based for, including auto, auto&, const auto&, and structured bindings.
// Tuple-like iteration β homogeneous types permitted as auto
template <typename Tuple>
void print_all(Tuple const& tup) {
template for (auto const& elem : tup) { // C++26
std::cout << elem << '\n';
}
}
// Non-type parameter pack β pack expansion directly in the range
template <int... Ns>
consteval int product() {
int result = 1;
template for (constexpr int n : Ns...) { // C++26
result *= n;
}
return result;
}
static_assert(product<2, 3, 5>() == 30);Examples
Heterogeneous tuple processing
The flagship use case: applying per-element operations across a tuple without std::apply or manual index sequences.
#include <tuple>
#include <iostream>
#include <string>
template <typename Tuple, typename F>
void for_each_element(Tuple&& tup, F&& f) {
template for (auto& elem : tup) { // C++26
f(elem);
}
}
int main() {
auto t = std::make_tuple(42, 3.14, std::string{"hello"});
for_each_element(t, [](auto const& v) {
std::cout << v << '\n';
});
// Output: 42 / 3.14 / hello
}Type-conditional dispatch
Because each iteration is a distinct instantiation, if constexpr can branch on the current element's type:
#include <type_traits>
#include <tuple>
#include <cstdio>
template <typename... Ts>
void describe_pack() {
template for (auto t : std::tuple<std::type_identity<Ts>...>{}) { // C++26
using T = typename decltype(t)::type;
if constexpr (std::is_integral_v<T>) std::puts("integral");
else if constexpr (std::is_floating_point_v<T>) std::puts("floating-point");
else if constexpr (std::is_same_v<T, std::string>) std::puts("string");
else std::puts("other");
}
}
// describe_pack<int, double, std::string>() β integral / floating-point / stringPre-C++26, achieving this required a helper struct with explicit partial specialisations, or an SFINAE-laden overload set called from inside a fold expression β neither of which scales to multi-statement bodies.
Replacing index-sequence boilerplate
The index-sequence pattern predates C++11 lambdas as a workaround; template for makes the indirection unnecessary:
// C++14/17 β two-function trampoline
template <typename Tuple, typename F, std::size_t... Is>
void apply_each_impl(Tuple& t, F& f, std::index_sequence<Is...>) {
(f(std::get<Is>(t)), ...); // C++17 fold β single expression only
}
template <typename Tuple, typename F>
void apply_each(Tuple& t, F f) {
apply_each_impl(t, f, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}
// C++26 β single function, multi-statement body permitted
template <typename Tuple, typename F>
void apply_each(Tuple& t, F f) {
template for (auto& elem : t) {
f(elem);
}
}Compile-time validation across a pack
#include <concepts>
#include <stdexcept>
template <typename... Ts>
void assert_trivially_copyable() {
template for (auto t : std::tuple<std::type_identity<Ts>...>{}) { // C++26
using T = typename decltype(t)::type;
static_assert(std::is_trivially_copyable_v<T>,
"All types in this pack must be trivially copyable");
}
}
// Fires a compile-time error for the offending type, not the whole pack at once
assert_trivially_copyable<int, float, std::string>(); // error on std::stringThe advantage over static_assert((std::is_trivially_copyable_v<Ts> && ...)) is that the failing type is identified individually, not as an undifferentiated pack.
Non-type pack accumulation
template <std::size_t... Dims>
consteval std::size_t flat_size() {
std::size_t n = 1;
template for (constexpr std::size_t d : Dims...) { // C++26
n *= d;
}
return n;
}
// Multi-dimensional array whose flat size is computed at compile time
template <std::size_t... Dims>
using TensorStorage = std::array<float, flat_size<Dims...>()>;
TensorStorage<3, 4, 5> t; // std::array<float, 60>Reflection Integration (P2996)
template for was designed alongside static reflection (P2996) β in fact, iterating over reflected member sequences is the primary motivation P1306 cites. Without an expansion statement, the std::meta::nonstatic_data_members_of range returns a runtime std::vector<std::meta::info>, which cannot drive per-member type operations. With template for, each std::meta::info value is a distinct compile-time constant, and the reflected name and type are available inside the body.
The canonical pattern is print_fields: given a struct type T, emit each field's name and value without any boilerplate beyond the loop itself.
#include <meta> // P2996 reflection header (C++26)
#include <iostream>
#include <string_view>
// Print every non-static data member of T by name and value.
template <typename T>
void print_fields(T const& obj) {
template for (constexpr std::meta::info m : // C++26
std::meta::nonstatic_data_members_of(^^T)) {
// ^^T is the reflection of type T (P2996 double-caret syntax)
constexpr std::string_view name = std::meta::name_of(m);
std::cout << name << " = " << obj.[:m:] << '\n';
// obj.[:m:] is the splice operator: dereferences the reflected member
}
}
struct Point { int x; int y; double z; };
int main() {
Point p{1, 2, 3.14};
print_fields(p);
// Output:
// x = 1
// y = 2
// z = 3.14
}The ^^T operator reflects type T into a std::meta::info value. std::meta::nonstatic_data_members_of(^^T) returns a compile-time range of std::meta::info values, one per member. The splice obj.[:m:] materialises the reflected member access back into ordinary C++ syntax. All three β ^^, nonstatic_data_members_of, and [:..:] β are P2996 facilities; template for is what makes them composable into a readable loop.
// Extended: build a map of field names to string representations
#include <meta>
#include <map>
#include <sstream>
#include <string>
template <typename T>
std::map<std::string, std::string> fields_as_strings(T const& obj) {
std::map<std::string, std::string> result;
template for (constexpr std::meta::info m : // C++26
std::meta::nonstatic_data_members_of(^^T)) {
std::ostringstream oss;
oss << obj.[:m:];
result[std::string(std::meta::name_of(m))] = oss.str();
}
return result;
}Without template for, writing fields_as_strings generically required a chain of if constexpr guards on manually listed index counts β a technique that breaks down entirely when the struct is not known at authoring time.
Structured Bindings in template for
Structured bindings work inside template for exactly as they do in range-based for. Each iteration still produces a distinct instantiation, so the bound names k and v have the types appropriate to that particular element β which may differ from iteration to iteration when the tuple holds heterogeneous pairs.
#include <tuple>
#include <utility>
#include <iostream>
template <typename... Keys, typename... Values>
void print_pairs(std::tuple<std::pair<Keys, Values>...> const& t) { // C++26
template for (auto const& [k, v] : t) {
std::cout << k << " -> " << v << '\n';
}
}
int main() {
auto pairs = std::make_tuple(
std::pair{std::string{"name"}, std::string{"Alice"}},
std::pair{std::string{"age"}, 42},
std::pair{std::string{"score"}, 9.8}
);
print_pairs(pairs);
// Output:
// name -> Alice
// age -> 42
// score -> 9.8
}The structured binding [k, v] is resolved per-element by the compiler using the same tuple-protocol rules as ordinary structured bindings: std::tuple_size, std::tuple_element, and std::get on the element type. Because each element here is a std::pair, k binds to .first and v to .second, but their concrete types are determined fresh each iteration.
Structured bindings combine especially well with reflection: you can iterate over reflected enumerators of an enum and destructure each into its name and value in a single loop line.
#include <meta>
template <typename E>
void list_enumerators() {
template for (constexpr auto [name, val] : // C++26
std::meta::enumerators_of(^^E)) {
// name is std::string_view, val is the underlying integer
std::cout << name << " = " << static_cast<int>(val) << '\n';
}
}Comparison with std::apply and index_sequence
Three approaches can express "call f on each element of a tuple." The table below compares them on a concrete example β printing every element β and then on qualitative dimensions.
The operation: print all elements of a tuple
// ββ Approach A: C++17 std::apply + fold expression ββββββββββββββββββββββββββ
#include <tuple>
#include <iostream>
template <typename Tuple>
void print_tuple_a(Tuple const& t) {
std::apply([](auto const&... elems) { // C++17
((std::cout << elems << '\n'), ...);
}, t);
}
// Constraint: single expression only inside the lambda body.
// Multi-statement processing requires an immediately-invoked helper.
// ββ Approach B: std::apply + std::index_sequence βββββββββββββββββββββββββββββ
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(Tuple const& t, std::index_sequence<Is...>) {
// Still limited to fold or recursive template inside impl
((std::cout << std::get<Is>(t) << '\n'), ...);
}
template <typename Tuple>
void print_tuple_b(Tuple const& t) { // C++14
print_tuple_impl(t, std::make_index_sequence<
std::tuple_size_v<Tuple>>{});
}
// Two functions required; the impl is not locally readable.
// ββ Approach C: C++26 template for βββββββββββββββββββββββββββββββββββββββββββ
template <typename Tuple>
void print_tuple_c(Tuple const& t) {
template for (auto const& elem : t) { // C++26
// Full statement body: local variables, if constexpr, loops, etc.
std::cout << elem << '\n';
}
}Comparison table
| Dimension | A: std::apply + fold | B: apply + index_sequence | C: template for |
|---|---|---|---|
| C++ version | C++17 | C++14 | C++26 |
| Multi-statement body | No | No (fold only in impl) | Yes |
| Local variables per element | No | No | Yes |
if constexpr inside body | No (needs overload) | No | Yes |
| Lines of code (simple case) | ~3 | ~8 | ~3 |
| Lines of code (complex body) | many (nested lambdas) | many (impl function) | same as simple |
| Compile-time error locality | Whole pack at once | Per-index | Per-element type |
| Works with reflection ranges | No | No | Yes |
| Readable without TMP knowledge | Moderate | Low | High |
For simple single-expression operations, approach A remains idiomatic because it requires no language extensions beyond C++17. For anything that needs a full statement body, local variables, or reflection ranges, template for is strictly superior.
Consteval Context
template for works naturally inside consteval functions, which is one of its most powerful use cases. A consteval function must be evaluatable entirely at compile time, and template for β being a purely compile-time construct β satisfies that requirement trivially. Fold expressions, by contrast, cannot contain multi-statement bodies, making complex consteval logic awkward to express.
The example below builds a sorted lookup array mapping enum names to their underlying values entirely at compile time:
#include <meta>
#include <array>
#include <string_view>
#include <algorithm>
template <typename E>
struct NameValue {
std::string_view name;
int value;
};
// consteval: the entire function runs at compile time; result is a constexpr array.
template <typename E>
consteval auto build_enumerator_table() { // C++26
constexpr std::size_t N = std::meta::enumerators_of(^^E).size();
std::array<NameValue<E>, N> table{};
std::size_t i = 0;
template for (constexpr std::meta::info m :
std::meta::enumerators_of(^^E)) {
table[i++] = {std::meta::name_of(m),
static_cast<int>([:m:])}; // splice the enumerator value
}
// Sort by value for binary search at runtime
std::sort(table.begin(), table.end(),
[](auto& a, auto& b){ return a.value < b.value; });
return table;
}
enum class Color { Red = 1, Green = 2, Blue = 4, Alpha = 8 };
constexpr auto color_table = build_enumerator_table<Color>();
// color_table is a sorted constexpr array; no runtime initialization cost.The contrast with fold expressions in consteval context is instructive. Fold expressions are limited to a single expression, so sorting the table, maintaining an index, or applying conditional logic requires either nested lambdas (which cannot be consteval) or separate helper functions. template for allows all of that logic to live in a single, linearly readable body.
// What you would need without template for β fragile and opaque:
template <typename E, std::size_t... Is>
consteval auto build_table_impl(std::index_sequence<Is...>) {
constexpr auto members = std::meta::enumerators_of(^^E);
return std::array{NameValue<E>{std::meta::name_of(members[Is]),
static_cast<int>([:members[Is]:]) }...};
// Cannot sort here β fold expansion, not a statement
}Best Practice: prefer template for over fold-in-consteval for any body that needs more than one expression. The loop body is a full compound statement, so sorting, logging, bounds-checking, and early diagnostics all fit naturally without auxiliary function templates.
Nested template for
Two template for loops can be nested to iterate over a tuple of tuples, producing a Cartesian product of instantiations. Each combination of outer-element type and inner-element type is a distinct instantiation, which makes the pattern powerful for cross-type operations.
#include <tuple>
#include <iostream>
#include <type_traits>
// Print the result of combining every pair of types from two packs.
template <typename... Ts, typename... Us>
void cartesian_describe() {
template for (auto t : std::tuple<std::type_identity<Ts>...>{}) { // C++26
using T = typename decltype(t)::type;
template for (auto u : std::tuple<std::type_identity<Us>...>{}) {
using U = typename decltype(u)::type;
if constexpr (std::is_convertible_v<T, U>) {
std::cout << typeid(T).name() << " -> "
<< typeid(U).name() << " (convertible)\n";
}
}
}
}
// cartesian_describe<int, double, char>() iterates 3Γ3 = 9 pairsA concrete use case is building a static dispatch table that maps every combination of two runtime type-tags to a compiled binary operation:
// Outline of a typed expression evaluator (C++26)
template <typename... Types>
struct BinaryDispatch {
template <typename L, typename R>
static void process_pair(L lhs, R rhs) {
// Specialised per (L, R) combination
std::cout << lhs + rhs << '\n'; // simplified
}
void dispatch_all(std::tuple<Types...> const& lhs_vals,
std::tuple<Types...> const& rhs_vals) {
template for (auto& l : lhs_vals) {
template for (auto& r : rhs_vals) {
process_pair(l, r);
}
}
}
};Common Pitfall: compile-time explosion from nested loops. If the outer pack has M elements and the inner has N, the compiler generates MΓN instantiations. For M = N = 10, that is 100 specialisations of the body. For M = N = 20, it is 400. If the body is non-trivial, compile times grow quadratically. The standard mitigation is to factor the inner body into a named function template and let the compiler deduplicate identical instantiations across outer iterations:
// Instead of a large nested body, delegate to a helper:
template <typename Outer, typename Inner>
void process_combination() {
// All logic for one (Outer, Inner) pair lives here.
// If Inner is the same type across multiple outer iterations,
// the compiler reuses the same specialisation.
}
template <typename... Ts, typename... Us>
void nested_dispatch() {
template for (auto t : std::tuple<std::type_identity<Ts>...>{}) {
template for (auto u : std::tuple<std::type_identity<Us>...>{}) {
process_combination<
typename decltype(t)::type,
typename decltype(u)::type>();
}
}
}Compiler Support and Godbolt
P1306 (expansion statements) and P2996 (static reflection) were voted into the C++26 working draft. As of mid-2026, compiler support is experimental and gated behind feature flags.
| Compiler | Version | P1306 status | P2996 status | Flag(s) |
|---|---|---|---|---|
| GCC | 15+ | Experimental | Experimental | -std=c++26 -freflection |
| Clang | 20+ | Partial | Partial | -std=c++26 -freflection -ftemplate-for |
| MSVC | 19.42+ | Under development | Under development | /std:c++latest /experimental:reflection |
| EDG (ICC) | 6.6+ | Under development | Prototype | vendor flags |
Trying it on Godbolt
The Compiler Explorer at godbolt.org hosts experimental compiler builds. To try the examples in this page:
- Select x86-64 clang (experimental) from the compiler list and choose a build labelled
trunkor20+. - Set the compiler flags to
-std=c++26 -freflection -ftemplate-for. - Add
#include <meta>for reflection-dependent examples. - For GCC experimental builds, use
-std=c++26 -freflection(no separate-ftemplate-forflag β both features share the flag).
Note that P1306 and P2996 are tightly interdependent in practice. Reflection without expansion statements is limited to single-expression splices; expansion statements without reflection reduce to a convenience over index-sequence tricks. The combination is what unlocks generic struct serialisation, enum-to-string mapping, and schema-driven code generation without any external tooling.
Best Practice: guard production code behind __cpp_reflection and __cpp_expansion_statements. Both features expose feature-test macros. A portable header can fall back to the std::apply + index_sequence approach when the macros are absent, enabling the same codebase to compile on C++17 or C++20 toolchains while taking advantage of C++26 where available.
// Portable fallback pattern (C++26)
#if defined(__cpp_reflection) && defined(__cpp_expansion_statements)
template <typename T>
void print_fields(T const& obj) {
template for (constexpr std::meta::info m :
std::meta::nonstatic_data_members_of(^^T)) {
std::cout << std::meta::name_of(m) << " = " << obj.[:m:] << '\n';
}
}
#else
// Fallback: requires user-provided specialisation or Boost.Describe
template <typename T>
void print_fields(T const& obj) {
static_assert(std::is_void_v<T>,
"print_fields requires C++26 reflection; specialise this template.");
}
#endifBest Practices
Use template for when fold expressions aren't expressive enough. Fold expressions remain the right tool for simple unary or binary operations ((f(args), ...), (args + ...)). Reach for template for when the body needs local variables, if constexpr branches, or multiple statements.
Factor large bodies into helper functions. Each iteration produces a distinct instantiation. A 50-line loop body over a 20-element pack generates 20 instantiation paths. Delegating to a process_one<T>() helper makes error messages and compile times more manageable.
Combine with if constexpr rather than overloaded helpers. The idiomatic pattern inside template for is to test type properties with if constexpr in the body rather than delegating to an overload set β the body is already in instantiation context, so the branch is free.
constexpr loop variables enable compile-time arithmetic. When iterating over non-type packs with constexpr auto n, n is usable as a template argument or array bound inside the body, which recursive templates or fold expressions cannot express cleanly.
Guard nested template for with factored-out helper templates to control instantiation count. For MΓN outerΓinner loops, placing the body in a named helper allows the compiler to deduplicate identical instantiations and produces cleaner error messages when one combination fails.
Use template for inside consteval for multi-statement compile-time algorithms. The combination produces code that is both maximally fast (zero runtime cost) and maximally readable β the body reads like ordinary imperative code with no fold-expression gymnastics.
Common Pitfalls
No early-exit semantics. template for always instantiates the body for every element. There is no break that skips remaining elements. If you need short-circuit evaluation, encode it as a runtime bool flag or restructure with if constexpr guards.
The loop variable changes type each iteration. Code that tries to accumulate the variable itself (rather than a derived value) will fail:
// ill-formed β no single type spans all iterations
template <typename... Ts>
void bad(std::tuple<Ts...>& t) {
auto last = ???;
template for (auto& elem : t) {
last = elem; // ERROR: type of elem differs each iteration
}
}Accumulate into a std::variant or type-erased wrapper if you need to carry a value out of the loop.
Diagnostics multiply. When a loop body fails to instantiate for one element, the compiler reports a failure at each offending instantiation separately. For a pack of 20 types, a missing member error can produce 20 near-identical error blocks. Name the loop body as a named function template to collapse the noise.
Tuple-like requires a complete type at the template for site. The compiler must see std::tuple_size and std::get for the range type at the point of expansion. Forwarding an incomplete type into a template for is ill-formed.
Reflection splice syntax is not yet stable across all vendors. The ^^T reflection operator and [:m:] splice syntax reflect P2996 as voted into the C++26 draft; earlier experimental compilers may use ^T (single caret) or __reflect. Check the compiler's release notes before relying on splice syntax in shared codebases.
See Also
reference/language/range-based-forβ the runtime analogue with identical declarative iteration syntaxreference/language/template-specializationβ explicit and partial specialisations, the patterntemplate formost often replaces in type-dispatch code