std::integer_sequence¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++14 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-11-03 |
编译器支持:GCC 4.9+ · Clang 3.4+ · MSVC 19.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 index_sequence 展开 tuple,把每个元素交给回调 | ev-mp-units-integer-sequence-tuple-001 |
| 用 index_sequence 把 JSON 数组下标展开为 tuple 元素 | ev-nlohmann-integer-sequence-tuple-002 |
| 用 index_sequence_for 展开一组异步任务并逐个启动 | ev-drogon-integer-sequence-tasks-003 |
本站证据页:ev-drogon-integer-sequence-tasks-003 · ev-mp-units-integer-sequence-tuple-001 · ev-nlohmann-integer-sequence-tuple-002
解决什么问题¶
可变参数模板把类型包 Ts... 保留到模板内部,但包里的每个元素没有下标,无法像数组那样用循环访问。要遍历 std::tuple、把一组参数逐个映射到函数、或为每种类型生成一个编号,只能靠递归模板一层层剥离参数,代码冗长、编译期深度也难控制。
std::integer_sequence<T, Values...> 是一个只存在于类型里的整数序列。它把 0、1、2 等下标编码成参数包,配合函数模板的参数推导,可以一次性展开所有下标,让编译器生成对应的 std::get 或函数调用。标准提案为 N3658,头文件是 <utility>。
基础用法¶
std::integer_sequence<T, 0, 2, 4> 表示一组编译期整数;std::index_sequence<Is...> 是 T = std::size_t 的别名,std::make_index_sequence<N> 生成 0..N-1,std::index_sequence_for<Ts...> 按类型包长度生成同样的序列。
#include <array>
#include <iostream>
#include <string>
#include <tuple>
#include <utility>
template <typename T, T... Values>
constexpr std::array<T, sizeof...(Values)> to_array(
std::integer_sequence<T, Values...>) {
return {{Values...}};
}
template <typename Tuple, typename F, std::size_t... Is>
void for_each_impl(const Tuple& tuple, F&& func, std::index_sequence<Is...>) {
int unused[] = {0, ((void)func(std::get<Is>(tuple)), 0)...};
(void)unused;
}
template <typename Tuple, typename F>
void for_each_tuple(const Tuple& tuple, F&& func) {
for_each_impl(tuple, func,
std::make_index_sequence<std::tuple_size<Tuple>::value>{});
}
int main() {
constexpr auto evens = to_array(std::integer_sequence<int, 0, 2, 4, 6>{});
static_assert(evens.size() == 4, "four values");
static_assert(evens[3] == 6, "last value");
int sum = 0;
for (int value : evens) {
sum += value;
}
std::cout << evens.size() << ' ' << sum << '\n';
const auto record = std::make_tuple(1, 2.5, std::string("atlas"));
for_each_tuple(record, [](const auto& value) { std::cout << value << ' '; });
std::cout << '\n';
std::cout << std::tuple_size<decltype(record)>::value << '\n';
}
输出 4 12、1 2.5 atlas(末尾有一个空格)与 3。to_array 把任意整数序列直接展开成 std::array;for_each_tuple 用 make_index_sequence 生成下标包,再在 for_each_impl 中展开成对每个元素的调用。完整版本见 examples/cpp14/integer_sequence.cpp。
进阶用法¶
把序列作为函数参数时,模板参数 Is... 会被推导出来;调用方只负责生成序列,展开逻辑集中在被调函数里。这种“生成序列—推导包—一次展开”的三段式是可复用写法,std::apply 内部也采用类似结构。
index_sequence_for<Ts...> 直接按类型包长度生成下标,适合 tuple 和 variant 这类元素个数由类型决定的容器。make_index_sequence<N> 则用于长度已知但元素类型无关的场景,例如把运行期数组复制成 std::array。
自定义 integer_sequence<T, ...> 可以携带非 size_t 的整数。to_array 示例用 int 序列直接生成数组;也能用 std::integer_sequence<char, 'a', 'b'> 在编译期构造字符表。
C++17 的折叠表达式可以简化展开体,但生成下标仍然需要 index_sequence。两者是互补关系:序列负责编号,折叠负责把包缩减成一条表达式。
在 C++14 之前,Abseil 与 nlohmann/json 都自带 integer_sequence 的兼容实现,用递归和偏特化模拟同样的接口;升级到 C++14 后只需切换到 std:: 版本。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| mp-units | src/core/include/mp-units/ext/algorithm.h#L167-L177 |
用 index_sequence 展开 tuple,把每个元素交给回调 |
序列生成与折叠展开分离,是 tuple 遍历的标准骨架 |
| nlohmann/json | single_include/nlohmann/json.hpp#L5230-L5234 |
用 index_sequence 把 JSON 数组下标展开成 tuple 元素 |
序列下标直接对应容器位置,避免手写每个字段 |
| drogon | lib/inc/drogon/utils/coroutine.h#L887-L892 |
用 index_sequence_for 展开一组异步任务并逐个启动 |
类型包长度决定任务数,下标在编译期完成分配 |
三条证据均为 manual 级:作者复核过出处与行号,尚未引入第二名审核者。三处都在生产路径,覆盖 tuple 遍历、序列化与任务批量启动。
陷阱与边界¶
序列只存在于类型里。 integer_sequence 没有运行期迭代器,不能 for 遍历;它的用途是让编译器展开参数包,而不是替代普通数组。
展开顺序不保证。 C++17 之前,花括号初始化列表中的求值顺序没有保证;依赖副作用的展开写法要改用折叠表达式或显式循环。
空包需要单独处理。 展开为空时,func(std::get<Is>(tuple))... 可能生成空表达式;用额外哨兵元素或偏特化处理 index_sequence<>。
下标越多,实例化越多。 每个长度生成独立函数模板,过大的序列会显著增加编译时间和二进制体积。
index_sequence 的元素类型固定。 它等价于 integer_sequence<std::size_t, ...>;需要 int 或其他类型时直接用 integer_sequence<T, ...>。
不能把序列当运行期容器。 从运行期变量生成序列需要 make_index_sequence<N>,其中 N 必须是常量表达式。
与 C++11 兼容层混用要小心。 项目自带的 index_sequence 与 std::index_sequence 是不同类型,模板重载可能产生歧义。
库联动¶
std::tuple:用下标序列展开std::get,是 tuple 遍历、序列化和哈希的基础。std::array:把integer_sequence直接展开成元素列表,编译期生成常量表。std::variant:按variant_size生成下标,逐个尝试类型分支。std::apply:C++17 用 tuple 元素作为函数实参,内部正是索引序列展开。- nlohmann/json:tuple 反序列化用序列下标读取 JSON 数组的对应位置。
- Abseil:
absl::integer_sequence为 C++11 项目提供同接口兼容层。
横向对比¶
| 方案 | 需要下标 | 编译期展开 | 代码量 | 主要代价 |
|---|---|---|---|---|
integer_sequence |
是 | 是 | 少 | 需要理解参数包推导 |
| 递归模板展开 | 否 | 是 | 多 | 模板深度大,错误信息难读 |
| 手写索引表 | 是 | 否 | 随元素数增长 | 元素数量变化就要改代码 |
| C++17 折叠表达式 | 否 | 是 | 少 | 只负责缩减,不负责生成下标 |
| 运行期循环 | 是 | 否 | 少 | 不能访问 tuple 的异质元素 |
跨标准演进:C++14 通过 N3658 引入 integer_sequence 与 index_sequence;C++17 的折叠表达式简化了展开体,但序列生成仍然必要;C++20 的模板 lambda 与 std::apply 进一步降低了手写展开的频率,底层机制未变。