Ch15.7: template for: Expansion Statements and Template Indexing
1. The Loop That Runs at Compile Time
A range-for over a ::std::tuple does not compile: the
elements have different types, so there is no common type for the loop variable. A
template for — an expansion statement —
does, because it is not one loop: the body is instantiated once per element, and
each instantiation sees its element's real type.
template <typename... Ts>
inline void print_tuple(::std::tuple<Ts...> const &t) throws
{
template for (auto const &e : t)
{
::fast_io::io::println(e);
}
}
print_tuple(::std::tuple<int, ::std::string_view, ::std::int_least64_t>{7, "seven", 7})
is three println calls after instantiation: one for int,
one for ::std::string_view, one for
::std::int_least64_t. Nothing is boxed, nothing is type-erased, and
there is no runtime loop counter to optimise away.
2. A constexpr Loop Variable
The loop variable may be declared constexpr, which makes every iteration
a compile-time constant. That is how you walk indices rather than elements:
inline void print_squares() throws
{
template for (constexpr ::std::size_t i : ::std::views::iota(::std::size_t{}, ::std::size_t{4}))
{
::fast_io::io::println(i, " squared is ", i * i);
}
}
i is usable as a template argument, an array bound, or a pack index.
Four iterations are four instantiations of the body, and the printing of
i is not an accident: i is a constant, so the compiler can
fold i * i and print a literal if it wants to.
Use a constexpr loop variable when the body needs the index at compile
time (to call ::std::get<I>, or to index a pack), and a plain
auto one when it only needs the element.
3. Template Indexing: args...[i]
A pack can be indexed directly. args...[1] is the second element, and
the subscript must be a constant expression:
template <typename... Ts>
[[nodiscard]] inline auto second(Ts... args) noexcept
{
static_assert(sizeof...(Ts) >= 2, "need at least two arguments");
return args...[1];
}
second(10u, 20u, 30u) returns the unsigned
20 — no recursion, no tuple, no ::std::get. Before
pack indexing existed, the same thing needed either a helper struct or a recursive
template with sizeof...(Ts) - 1 arithmetic and one instantiation per
element.
args...[1] is a compile-time subscript. The one runtime-looking
exception is indexing by a constexpr loop variable, which is still a
constant per iteration:
template for (constexpr ::std::size_t i : ::std::views::iota(::std::size_t{}, sizeof...(Args)))
{
using arg_type = ::std::remove_cvref_t<Args...[i]>;
// ... one instantiation per i
}
4. How fast_io Itself Uses It
This is not a language-lawyer corner. ::fast_io::io::print has to know,
for every argument, whether it is directly printable or has to be printed through a
temporary buffer — and it has to know this before it writes a single byte. The
real code from
fast_io_core_impl/operations/printimpl/print_freestanding.h is:
template <::std::integral char_type, typename... Args>
inline consteval ::fast_io::details::first_print_define_index_range_result
first_print_define_index_range() noexcept
{
::std::size_t startpos{sizeof...(Args)};
::std::size_t endpos{sizeof...(Args)};
template for (constexpr auto i :
::fast_io::details::index_array_range<0, sizeof...(Args)>)
{
using ArgsIType = ::std::remove_cvref_t<Args...[i]>;
if constexpr (::fast_io::reserve_printable<char_type, ArgsIType> ||
::fast_io::dynamic_reserve_printable<char_type, ArgsIType> ||
::fast_io::scatter_printable<char_type, ArgsIType> ||
::fast_io::context_printable<char_type, ArgsIType> ||
::fast_io::printable<char_type, ArgsIType>)
{
startpos = i;
break;
}
}
// ...
}
Three things are happening, all at compile time. first_print_define_index_range
is consteval, so it is evaluated during compilation and leaves no code.
template for unrolls over the argument positions.
Args...[i] names the type of each position. The break
stops the unrolling, so the answer is the position of the first argument that can be
printed directly — the fact print needs to decide how to interleave
direct writes with buffered ones.
The consequence for users: a compiler that cannot compile expansion statements cannot compile fast_io at all. This is one of the two C++26 features the library genuinely cannot do without (the other is herbceptions).
5. The Older Way: index_sequence
If you have to support a toolchain without expansion statements, the idiom is
::std::index_sequence: a type that carries
0, 1, ..., N-1 as template arguments, expanded into a pack of indices
which is itself folded:
template <typename... Ts, ::std::size_t... I>
inline void print_indexed(::std::index_sequence<I...>, ::std::tuple<Ts...> const &t) throws
{
(::fast_io::io::println(I, ": ", ::std::get<I>(t)), ...);
}
template <typename... Ts>
inline void print_indexed(::std::tuple<Ts...> const &t) throws
{
print_indexed(::std::make_index_sequence<sizeof...(Ts)>{}, t);
}
The trick is that I... and Ts... are expanded in lockstep:
::std::get<I>(t) with I and t both
expanding gives ::std::get<0>, ::std::get<1>,
::std::get<2> in one fold. It works, and it costs an extra
function per call.
Learn it for reading old code, not for writing new code. template for is
shorter, has one fewer instantiation, and says what it means.
6. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <tuple>
#include <utility>
#include <ranges>
template <typename... Ts>
inline void print_tuple(::std::tuple<Ts...> const &t) throws
{
template for (auto const &e : t)
{
::fast_io::io::println(e);
}
}
inline void print_squares() throws
{
template for (constexpr ::std::size_t i : ::std::views::iota(::std::size_t{}, ::std::size_t{4}))
{
::fast_io::io::println(i, " squared is ", i * i);
}
}
template <typename... Ts>
[[nodiscard]] inline auto second(Ts... args) noexcept
{
static_assert(sizeof...(Ts) >= 2, "need at least two arguments");
return args...[1];
}
template <typename... Ts, ::std::size_t... I>
inline void print_indexed(::std::index_sequence<I...>, ::std::tuple<Ts...> const &t) throws
{
(::fast_io::io::println(I, ": ", ::std::get<I>(t)), ...);
}
template <typename... Ts>
inline void print_indexed(::std::tuple<Ts...> const &t) throws
{
print_indexed(::std::make_index_sequence<sizeof...(Ts)>{}, t);
}
int main()
{
try
{
print_tuple(::std::tuple<int, ::std::string_view, ::std::int_least64_t>{7, "seven", 7});
print_squares();
::fast_io::io::println(second(10u, 20u, 30u));
print_indexed(::std::tuple<int, ::std::string_view, ::std::int_least64_t>{7, "seven", 7});
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
7
seven
7
0 squared is 0
1 squared is 1
2 squared is 4
3 squared is 9
20
0: 7
1: seven
2: 7
7. Rules of Thumb
template forreplaces recursion for heterogeneous sequences; prefer it to a recursive variadic function whenever the body is the same for every element.- Declare the loop variable
constexprwhen you need it as a template argument or pack index; plainautootherwise. - An expansion statement is still a loop in the source and N instantiations in the binary: a body of fifty lines over twenty elements is a thousand lines of machine code. Keep the body short (Ch15.9).
- Use pack indexing (
args...[i]) instead of::std::get<I>(::std::tuple{args...}); it does not build a tuple. - Know
index_sequencefor reading legacy code, but do not teach it to new code.