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