第15.7章 template for:展开语句与模板下标

English

1. 在编译期跑的循环

对 ::std::tuple 用范围 for 编译不过:元素类型不同,循环变量 没有共同类型。template for —— 展开语句 —— 可以, 因为它不是一个循环:函数体按元素各实例化一次,每次都能看到该元素的真实类型。


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}) 实例化之后就是三次 println:一次 int、一次 ::std::string_view、一次 ::std::int_least64_t。 没有装箱、没有类型擦除,也没有需要优化掉的运行期计数器。

2. constexpr 的循环变量

循环变量可以声明为 constexpr,于是每次迭代都是编译期常量。走下标 而不是元素时就这么写:


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 可以当模板实参、数组界、包下标使用。四次迭代就是函数体的四次实例化, 而 i 能被打印并不是巧合:i 是常量,编译器完全可以把 i * i 折出来打印字面量。

函数体在编译期需要下标时(调用 ::std::get<I>、给包取下标)用 constexpr 循环变量;只需要元素时用普通的 auto。

3. 模板下标:args...[i]

包可以直接取下标。args...[1] 就是第二个元素,下标必须是常量表达式:


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) 返回 unsigned 的 20 —— 没有递归、没有 tuple、没有 ::std::get。在包下标出现之前,同样的事情要么靠 一个辅助结构体,要么靠带 sizeof...(Ts) - 1 算术的递归模板, 每个元素一层实例化。

args...[1] 是编译期下标。唯一看起来像运行期的是用 constexpr 循环变量取下标,但每次迭代它仍是常量:


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. fast_io 自己怎么用它

这不是语言律师的角落。::fast_io::io::print 必须知道:每个实参是可以直接打印, 还是必须经过临时缓冲区 —— 而且它必须在写出第一个字节之前就知道。真正的代码位于 fast_io_core_impl/operations/printimpl/print_freestanding.h:


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;
		}
	}
	// ...
}

三件事同时发生,全在编译期。first_print_define_index_range 是 consteval,所以它在编译时求值、不留代码。template for 按实参位置展开。Args...[i] 给出每个位置的类型。break 停止展开,于是答案就是“第一个能直接打印的实参位置” —— 这正是 print 用来决定如何把直接写与缓冲写交织起来的那个事实。

对用户来说的结论是:编译不了展开语句的编译器,根本编译不了 fast_io。这是库真正无法缺少的 两个 C++26 特性之一(另一个是赫布异常)。

5. 老写法:index_sequence

如果必须支持没有展开语句的工具链,惯用法是 ::std::index_sequence:一个把 0, 1, ..., N-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);
}

诀窍是 I... 与 Ts... 同步展开: ::std::get<I>(t) 里的 I 与 t 同时展开, 一次折叠就得到 ::std::get<0>、::std::get<1>、 ::std::get<2>。它确实能用,代价是每次调用多一个函数。

学它是为了读懂老代码,不是为了写新代码。template for 更短、少一层实例化, 而且它说的就是它做的。

6. 完整程序


#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);
	}
}

输出:


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. 经验法则