前言std::index_sequence是 C14 引入的编译期整数序列定义在utility中。它最常见的用途是配合可变参数模板把「一包类型或值」和「一串下标」对应起来从而对std::tuple、std::pair这类异构容器的每个元素做同样的操作。std::apply、std::make_from_tuple这些 C17 标准设施内部都依赖它。对它的误解主要有三个。第一把它当成运行期容器想对它size()之后循环遍历——它是纯编译期的类型序列本身不占运行期存储值只存在于模板参数里。第二以为std::make_index_sequenceN生成的是1..N。它生成的是0..N-1正好对应下标。第三以为它只能用于 C17。integer_sequence及其别名是 C14 的特性C17 只是新增了std::apply等使用者并让折叠表达式简化了展开写法。本文先列出相关的四个别名及其精确含义再解释「为什么需要下标包」然后用三个例子展示典型用法最后给出一个可移植的手写实现帮助理解标准库在背后做了什么。示例以 C17 为基准涉及 C14 限制的地方会单独说明。一、定义与四个别名标准对std::integer_sequence的定义可以简化为// 适用于 C14namespace std {template class T, T... Intsstruct integer_sequence {using value_type T;static constexpr std::size_t size() noexcept { return sizeof...(Ints); }};template std::size_t... Intsusing index_sequence integer_sequencestd::size_t, Ints...;template class T, T Nusing make_integer_sequence integer_sequenceT, 0, 1, /* ... */, N - 1;template std::size_t Nusing make_index_sequence make_integer_sequencestd::size_t, N;template class... Tsusing index_sequence_for make_index_sequencesizeof...(Ts);}四个名字的关系如下表名字含义典型写法integer_sequenceT, Ints...以T为元素类型的整数序列很少直接写index_sequenceInts...元素类型固定为std::size_t的序列std::index_sequence0, 1, 2{}make_index_sequenceN生成0, 1, ..., N-1std::make_index_sequence3{}index_sequence_forTs...生成与类型包等长的下标std::index_sequence_forint, double{}注意make_index_sequenceN的结果是index_sequence0, 1, ..., N-1长度恰好是N。当N为 0 时得到index_sequence这是合法的空包很多展开写法在空包下依然成立。这些别名都是类型使用时通常要构造一个临时对象末尾的{}传给函数让编译器从对象类型反推出整个参数包。这是它们最关键的使用方式。二、为什么需要下标包C 的参数包展开有一个硬性规则展开的模式里必须出现包名。对std::tuple来说元素是用std::getI(t)取出的其中I必须是一个编译期常量。如果只有一个类型包Ts...你没法直接写出std::get???(t)——类型包里没有下标。index_sequence提供的正是这样一包下标。把std::index_sequenceI...作为函数参数编译器会从实参类型推导出I...于是函数体内就能写std::getI(t)...把包展开成std::get0(t), std::get1(t), ...。这种手法常被称为「双包展开」一个包是类型或值另一个包是下标两者通过同一个...同步展开。展开时两个包的长度必须一致这也是为什么通常用index_sequence_forTs...生成下标——它保证长度与类型包相同。// 适用于 C17#include iostream#include string#include tuple#include utilitytemplate class Tuple, std::size_t... Ivoid print_impl(const Tuple t, std::index_sequenceI...) {((std::cout (I 0 ? : , ) std::getI(t)), ...);std::cout \n;}template class... Tsvoid print_tuple(const std::tupleTs... t) {print_impl(t, std::index_sequence_forTs...{});}int main() {auto t std::make_tuple(1, 2.5, std::string(hi));print_tuple(t);return 0;}print_tuple本身不知道 tuple 有几个元素它把类型包Ts...交给index_sequence_for后者算出长度并生成0..N-1print_impl再把I...和t一起展开。逗号折叠表达式((...), ...)保证每个元素都被求值一次空包时折叠结果为空不会输出多余内容。在 C14 里没有折叠表达式可以用数组初始化的技巧达到同样效果// 适用于 C14template class Tuple, std::size_t... Ivoid print_impl_cxx14(const Tuple t, std::index_sequenceI...) {int dummy[] {0, ((std::cout std::getI(t) ), 0)...};(void)dummy;}三、三个典型场景1. 把 tuple 展开成函数实参这是std::apply的原理。C17 已经提供了std::apply下面的代码主要用于理解机制。// 适用于 C14 及以上#include tuple#include utilitytemplate class F, class Tuple, std::size_t... Idecltype(auto) apply_impl(F f, Tuple t, std::index_sequenceI...) {return std::forwardF(f)(std::getI(std::forwardTuple(t))...);}template class F, class Tupledecltype(auto) apply(F f, Tuple t) {using indices std::make_index_sequencestd::tuple_sizetypename std::decayTuple::type::value;return apply_impl(std::forwardF(f), std::forwardTuple(t), indices{});}// 调用apply([](int a, double b) { return a b; }, std::make_tuple(1, 2.5));std::forwardTuple(t)保证 tuple 以原有的值类别传给std::get左值 tuple 取出左值引用右值 tuple 取出右值引用配合std::forwardF实现完美转发。std::decay去掉引用和const才能正确取到tuple_size。2. 把下标序列变成数组index_sequence的元素是编译期常量可以直接用来初始化std::array得到一个包含0..N-1的数组。// 适用于 C14 及以上#include array#include cstddef#include iostream#include utilitytemplate std::size_t... Iconstexpr auto make_indices(std::index_sequenceI...) {return std::arraystd::size_t, sizeof...(I){I...};}int main() {constexpr auto arr make_indices(std::make_index_sequence5{});for (std::size_t v : arr) {std::cout v ;}std::cout \n;return 0;}这里{I...}把包展开成初始化列表sizeof...(I)给出长度。整个函数可以声明为constexpr因为std::array的构造和下标序列都是编译期可求值的。C14 放宽了constexpr函数的限制这类写法才成为可能。3. 按下标访问多个异构容器有时需要同时遍历两个长度相同但类型不同的结构比如一个tuple和一个数组。用下标包可以保证访问一一对应。// 适用于 C17#include array#include iostream#include string#include tuple#include utilitytemplate class Tuple, class Array, std::size_t... Ivoid zip_print(const Tuple t, const Array a, std::index_sequenceI...) {((std::cout std::getI(t) a[I] \n), ...);}int main() {auto names std::make_tuple(std::string(cpu), std::string(mem));std::arrayint, 2 values{4, 8};zip_print(names, values, std::index_sequence_forstd::string, std::string{});return 0;}这里用index_sequence_forstd::string, std::string生成两个下标和 tuple 的长度一致。更稳健的写法是直接用std::tuple_sizedecltype(names)::value推导长度避免手写类型列表和实际 tuple 不一致。四、标准库背后做了什么标准没有规定make_index_sequence必须如何实现只规定结果。常见实现有两条路编译器内建和递归展开。编译器内建是主流做法。GCC 和 Clang 提供__integer_pack(N)MSVC 提供__make_integer_seq标准库直接用它们一次性生成序列复杂度是 O(1) 的模板实例化。这样即使N很大也不会触发模板深度限制。递归展开是可移植但较慢的写法适合用来理解原理// 适用于 C14#include cstddeftemplate class T, T... Intsstruct my_integer_sequence {using value_type T;static constexpr std::size_t size() noexcept { return sizeof...(Ints); }};template class T, T N, T... Intsstruct make_seq_impl : make_seq_implT, N - 1, N - 1, Ints... {};template class T, T... Intsstruct make_seq_implT, 0, Ints... {using type my_integer_sequenceT, Ints...;};template class T, T Nusing my_make_integer_sequence typename make_seq_implT, N::type;// my_make_integer_sequencestd::size_t, 4 得到序列 0, 1, 2, 3推演一下make_seq_implsize_t, 4继承make_seq_implsize_t, 3, 3再继承make_seq_implsize_t, 2, 2, 3依次得到make_seq_implsize_t, 0, 0, 1, 2, 3命中终止特化type就是integer_sequencesize_t, 0, 1, 2, 3。注意模板参数里N - 1和Ints...的拼接顺序写反了会得到倒序序列。递归实现的缺点是实例化深度等于N。大多数编译器默认的模板递归深度在几百到一千层左右make_index_sequence1000就可能报错。生产代码应使用标准库的实现不要自己递归。常见坑点❌ 把index_sequence当运行期容器写for (auto i : seq)。✅ 它只在编译期存在要运行期的0..N-1用std::array或普通循环。❌ 以为std::make_index_sequence3是1, 2, 3。✅ 它是0, 1, 2长度等于模板实参3。❌ 传类型而不是对象print_impl(t, std::index_sequence_forTs...)。✅ 加{}构造临时对象std::index_sequence_forTs...{}否则无法推导I...。❌ 在 C14 项目里使用逗号折叠表达式展开参数包。✅ 折叠表达式是 C17 特性C14 用数组初始化或递归函数展开。❌ 手写的递归make_index_sequence用在几百以上的N触发模板深度限制。✅ 使用标准库的std::make_index_sequence主流实现用编译器内建生成。❌ 把两个长度不同的包放进同一个展开式f(a[I]..., std::getJ(t)...)。✅ 两个包必须在同一次展开中长度一致用index_sequence_for统一长度或分开两次展开。❌ 把自定义的integer_sequence放进namespace std里「补全」标准库。✅ 向std添加声明是未定义行为自定义类型放在自己的命名空间。❌ 用std::getI时I来自运行期变量。✅ 模板实参必须是常量表达式运行期下标要用std::get之外的访问方式如std::variant的get或自行遍历。总结项目说明头文件utility引入版本C14integer_sequence及全部别名本质编译期类型序列元素在模板参数中核心用途为参数包提供下标实现双包同步展开关键别名index_sequence、make_index_sequence、index_sequence_for生成范围make_index_sequenceN生成0..N-1常见搭档std::tuple、std::get、完美转发、折叠表达式C17 替代std::apply、std::make_from_tuple已封装常见场景index_sequence本身只有十几行定义却解决了可变参数模板里「没有下标可用」的根本问题。理解它的关键不是记住别名而是理解「用类型推导出包」这一手法把序列作为函数参数让编译器替你数出0..N-1。掌握了这一点tuple 展开、编译期数组生成、异构容器同步访问都会变得顺理成章。新项目里如果只是想把 tuple 传给函数直接用 C17 的std::apply即可不必重复造轮子。
阅读完成 · 觉得有帮助?