Skip to content
C++
Language
since C++26
Advanced

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++26

An 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.

cpp
// 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

cpp
template for ( for-range-declaration : for-range-initializer ) statement

The 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.

cpp
// 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.

cpp
#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:

cpp
#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 / string

Pre-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:

cpp
// 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

cpp
#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::string

The 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

cpp
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>

Best 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.

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:

cpp
// 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.

See Also

  • reference/language/range-based-for β€” the runtime analogue with identical declarative iteration syntax
  • reference/language/template-specialization β€” explicit and partial specialisations, the pattern template for most often replaces in type-dispatch code