第15.7章 template for:展开语句与模板下标
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. 经验法则
- 异质序列上,
template for取代递归;只要函数体对每个元素相同,就优先用它。 - 需要把循环变量当模板实参或包下标时,声明为
constexpr;否则用普通auto。 - 展开语句在源码里是一个循环,在二进制里是 N 次实例化:二十个元素配五十行函数体 就是一千行机器码。让函数体保持短小 (第15.9章)。
- 用包下标(
args...[i])代替::std::get<I>(::std::tuple{args...});前者不构造 tuple。 - 为读老代码而了解
index_sequence,但不要教给新代码。