C++integer_sequence整数序列
integer_sequence:C++里那个不声不响却撑起元编程骨架的“整数清单”
你有没有写过这样的模板函数:
template<typename T, size_t... Is>
void print_tuple_impl(const std::tuple<T...>& t, std::index_sequence<Is...>) {
((std::cout << std::get<Is>(t) << " "), ...);
}
调用时还得手动传一个 std::make_index_sequence<sizeof...(T)>() —— 这个“手动传”的动作,背后站着的就是 std::integer_sequence。它不是炫技的玩具,而是 C++14 引入后,真正让参数包展开、索引映射、编译期计数变得可读、可复用、可推导的底层支点。
别被名字唬住。“整数序列”听起来像数学作业,其实它就是一个编译期固定的整型值列表容器,不占运行时内存,不构造对象,连 sizeof 都是 1。它的本质,是把一串数字(比如 0, 1, 2, 3)打包成一个类型,让模板系统能“看见”并解包它们。
最直白的写法是:
using seq = std::integer_sequence<int, 10, 20, 30>;
这定义了一个类型 seq,它“记住”了三个 int 值。但你几乎不会这么用——因为手动列数字违背了它的设计初衷。它的价值,在于配合 make_integer_sequence 和 index_sequence 自动生成连续序列,从而把“我要访问第 0 到 N-1 个元素”这个意图,直接编码进类型系统。
举个实在的例子:你想把 std::array<int, 5> 的每个元素加 1,并返回新数组。不用循环,不用 std::transform,纯编译期搞定:
template<typename T, size_t N, size_t... Is>
constexpr std::array<T, N> add_one_impl(
const std::array<T, N>& a, std::index_sequence<Is...>) {
return {a[Is] + 1...}; // 展开为 a[0]+1, a[1]+1, ..., a[N-1]+1
}
template<typename T, size_t N>
constexpr std::array<T, N> add_one(const std::array<T, N>& a) {
return add_one_impl(a, std::make_index_sequence<N>{});
}
这里 std::make_index_sequence<N> 返回的,就是 std::index_sequence<0,1,2,...,N-1> —— 而 index_sequence 本身,就是 integer_sequence<std::size_t, ...> 的别名。它把“长度 N”这个信息,无损地转化成了“可用的下标集合”这个类型。
很多人卡在第一步:为什么不能直接 for (size_t i = 0; i < N; ++i)?因为 N 是非类型模板参数,是编译期常量,但 for 是运行时控制流,编译器不允许在 constexpr 函数里用它遍历(C++20 前尤其严格)。integer_sequence 提供的,是唯一被标准认可的、合法且零开销的编译期索引枚举方式。
再深一层:它解决了“类型擦除式展开”的痛点。比如转发一个变参构造函数到内部 tuple:
template<typename... Args>
MyClass(Args&&... args)
: data{std::forward<Args>(args)...} {} // OK,但无法做额外逻辑
// 想在构造时对每个参数做预处理?得靠 sequence:
template<typename... Args>
MyClass(Args&&... args)
: MyClass(std::forward<Args>(args)...,
std::index_sequence_for<Args...>{}) {}
template<typename... Args, size_t... Is>
MyClass(Args&&... args, std::index_sequence<Is...>)
: data{preprocess(std::get<Is>(std::tuple<Args...>{std::forward<Args>(args)...}))...} {}
注意 std::index_sequence_for<Args...> —— 它等价于 make_index_sequence<sizeof...(Args)>,但更语义清晰:“为这些参数生成对应索引”,而不是“生成长度为 N 的序列”。这种命名差异,恰恰反映了它的实用心智模型:它永远服务于“已有参数包”,而非孤立存在。
有个容易忽略的细节:integer_sequence 的 value_type 可以是任意整型(int、long long、size_t),但 index_sequence 固定用 size_t。这意味着如果你需要负数索引(比如实现环形缓冲区偏移),得自己写 make_integer_sequence<int, N> 的变体——标准库没提供,但实现就三行,且完全合法。
最后说句掏心窝的:别把它当“高级技巧”供起来。在现代 C++ 项目里,只要涉及 tuple 操作、结构化绑定适配、序列化字段遍历、甚至某些 constexpr 容器实现,integer_sequence 就是那个沉默的螺丝钉。它不抢眼,但一旦抽掉,整个元编程骨架会松动。
下次看到 index_sequence,别条件反射去查文档。停下来想一想:此刻我手里的参数包,需要哪一组下标来安全展开? 答案往往就藏在 make_ 开头的那个调用里。


还没有评论,来说两句吧...