CppCon 2025 学习:How to Tame Packs, std:tuple, and the Wily std:integer_sequence
功能C++17/20C++26遍历参数包只能用或递归模板直接遍历结构化绑定只能解构 tuple/array[...elem]直接生成 pack编译期循环复杂模板递归自然语法易用性较低高,可直接写print_all或map操作 packAI 工具推荐:大多数会推荐“冗长的 helper 函数版本”通用原则尽量远离的繁琐用法构建简单的迭代原语(iteration primitive)用于 tuple、
作为参会者,你有权对任何会议演讲提出以下问题:
- 这份材料能帮助我使用 AI 编程助手吗?
- 这份材料能帮助我与 AI 编程助手协作吗?
- 这份材料能帮助我编写实际的 AI 工件吗?
MSB(会议组织者提示):
Please Stop Using Chatbots!
Evidence: https://github.com/NVIDIA/cccl/pull/5671
2. 动机(Motivation)
理解:
- 元组(tuple) 在 AI、科学计算和加速数学核(kernels)中非常重要。
- 参数包(parameter pack) 是一种特殊类型,但地位类似“弃儿”(使用不方便)。
- 参数包无法赋予名字,也不能像普通对象那样整体操作。
- 历史上,编译期没有方法遍历参数包(不能在编译期迭代)。
- C++26 的 P1306 提案 会解决这个问题。
- 现有的
std::integer_sequence只是一个笨拙的权宜之计。 - 参数包存在的所有缺点都还在,并且还有额外缺点。
总结:
C++26 引入的新机制允许在编译期更自然地操作参数包或类似容器,类似元组或数组。
3. 未来 C++26 的 template for
原文示例:
// T can be an expression list, a destructurable (std::tuple, std::array),
// or a range with compile-time size
template <class T>
void print_all(const T& t) {
template for (auto&& elem : t) { // or {t...} for packs
std::println("{}", elem);
}
}
理解:
T可以是:- 表达式列表(expression list)
- 可解构类型(如
std::tuple,std::array) - 编译期已知大小的 range(范围)
template for是 C++26 的新语法,可以在编译期迭代参数包或元组等类型。- 对于参数包,你可以写
{t...},将 pack 展开,然后迭代每个元素。 std::println是假想的打印函数,这里可以类比为fmt::print或std::cout。
示例化(C++26 风格伪代码):
#include <tuple>
#include <array>
#include <iostream>
template <class T>
void print_all(const T& t) {
template for (auto&& elem : t) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::tuple<int,double,char> tup{1,3.14,'x'};
print_all(tup); // 输出: 1 3.14 x
}
4. “结构化绑定引入 pack”
原文示例:
template <class T>
void print_all(const T& t) {
auto& [...elem] = t;
(std::println("{}", elem), ...);
}
理解:
- C++26 允许用 结构化绑定(structured bindings) 解构 tuple/array,并直接生成一个 pack:
[...elem] - 然后可以使用 fold expression
(f(...), ...)方式操作每个元素。
示例化(C++26 风格伪代码):
#include <tuple>
#include <iostream>
template <class T>
void print_all(const T& t) {
auto& [...elem] = t; // 将 tuple/array 拆成 pack
(std::cout << ... << elem) << "\n"; // fold expression 输出每个元素
}
int main() {
std::tuple<int,double,char> tup{1,3.14,'x'};
print_all(tup); // 输出: 13.14x
}
注意 fold expression
(std::cout << ... << elem)会把每个元素依次打印,可以配合空格做美化。
5. 总结
C++26 对模板和参数包的改进:
| 功能 | C++17/20 | C++26 |
|---|---|---|
| 遍历参数包 | 只能用 std::integer_sequence 或递归模板 |
template for 直接遍历 |
| 结构化绑定 | 只能解构 tuple/array | [...elem] 直接生成 pack |
| 编译期循环 | 复杂模板递归 | 自然语法 template for |
| 易用性 | 较低 | 高,可直接写 print_all 或 map 操作 pack |
1. 遍历参数包与元组(Tuple)
理解:
- 要遍历参数包或
std::tuple,需要一个辅助的索引序列std::integer_sequence。 std::index_sequence<Is...>就是这种索引序列,可以和 tuple 的元素一一对应。
1.1 C++20 常用方式(带 helper 函数)
namespace detail {
template <typename Tuple, size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
}
template<typename... Args>
void print_tuple(const std::tuple<Args...>& t) {
detail::print_tuple_impl(t, std::index_sequence_for<Args...>{});
}
注释:
std::index_sequence<Is...>是一个编译期整数序列,用来索引 tuple 元素:- 例如:tuple 有 3 个元素,序列就是
0,1,2 - 对应 t 0 , t 1 , t 2 t_0, t_1, t_2 t0,t1,t2
- 例如:tuple 有 3 个元素,序列就是
std::get<Is>(t)用来访问 tuple 中索引为Is的元素。((...), ...)是 fold expression:- 遍历每个索引
Is,按顺序打印 tuple 元素。
- 遍历每个索引
std::index_sequence_for<Args...>自动生成参数包长度的索引序列。
示例输出:
auto t = std::make_tuple(1, 3.14, 'x');
print_tuple(t); // 输出: 1, 3.14, x
1.2 C++20 “无 helper 函数”的小技巧
template <typename Tuple>
void print_tuple(const Tuple& t) {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}(std::make_index_sequence<std::tuple_size_v<Tuple>>());
}
理解:
- 利用 lambda 模板 和 索引包(Is…)
std::make_index_sequence<std::tuple_size_v<Tuple>>():- 自动生成从 0 到
tuple_size-1的整数序列 - 对应每个 tuple 元素索引
- 自动生成从 0 到
- 整个迭代逻辑放在 lambda 内,不需要额外 helper 函数
- 优点:函数定义更少,可直接在函数体内操作 tuple
示例:
auto t = std::make_tuple(10, 20, 30);
print_tuple(t); // 输出: 10, 20, 30
1.3 C++17 版本(带默认参数的技巧)
template <typename Tuple, size_t... Is>
void print_tuple(const Tuple& t, std::index_sequence<Is...> = {}) {
if constexpr (sizeof...(Is) != std::tuple_size_v<Tuple>) {
print_tuple(t, std::make_index_sequence<std::tuple_size_v<Tuple>>());
} else {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
}
理解:
- 利用函数默认参数
{}自动触发索引序列推导 - 当索引包
Is...的大小不等于 tuple 元素数量时,递归调用自身并生成正确序列 - 否则使用 fold expression 打印 tuple 元素
- 这种方法避免手动写 helper 函数,但略显“笨拙”
2. 总结与建议
- AI 工具推荐:大多数会推荐“冗长的 helper 函数版本”
- 通用原则:
- 尽量远离
std::integer_sequence的繁琐用法 - 构建简单的迭代原语(iteration primitive)
- 用于 tuple、array、参数包都很方便
- 尽量远离
- 核心概念:
- tuple 或参数包 + 索引序列 = 可以遍历
- fold expression 可以展开操作
- C++20+ lambda template + index_sequence = 最短的写法
3. 公式化思考
遍历 tuple 可以理解为:
for i ∈ [ 0 , N − 1 ] : print ( t i ) \text{for } i \in [0, N-1]: \quad \text{print}(t_i) for i∈[0,N−1]:print(ti)
其中 N = tuple_size ( t ) N = \text{tuple\_size}(t) N=tuple_size(t), t i = get< i > ( t ) t_i = \text{get<}i\text{>}(t) ti=get<i>(t)。
fold expression 形式:
( ( std::cout < < ( i = = 0 ? " " : " , " ) < < t i ) , . . . ) ((\text{std::cout} << (i==0?"":", ") << t_i), ...) ((std::cout<<(i==0?"":",")<<ti),...)
#include <iostream>
#include <tuple>
#include <array>
// ==============================
// 1. C++17 版本:带默认参数递归 + fold expression
// ==============================
template <typename Tuple, size_t... Is>
void print_tuple_cxx17(const Tuple& t, std::index_sequence<Is...> = {}) {
if constexpr (sizeof...(Is) != std::tuple_size_v<Tuple>) {
// 生成索引序列并递归调用
print_tuple_cxx17(t, std::make_index_sequence<std::tuple_size_v<Tuple>>());
} else {
// fold expression 打印 tuple 元素
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
std::cout << std::endl;
}
}
// ==============================
// 2. C++20 版本:lambda + index_sequence
// ==============================
template <typename Tuple>
void print_tuple_cxx20(const Tuple& t) {
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
std::cout << std::endl;
}(std::make_index_sequence<std::tuple_size_v<Tuple>>());
}
// ==============================
// 3. C++26 版本:结构化绑定 + pack iteration (假想)
// ==============================
#if __cplusplus >= 202600L
template <typename T>
void print_tuple_cxx26(const T& t) {
auto& [...elem] = t; // 结构化绑定直接展开 pack
(std::cout << elem << " ", ...);
std::cout << std::endl;
}
#endif
int main() {
// 定义一个 tuple,类型混合
auto t = std::make_tuple(42, 3.14, 'x', "hello");
std::cout << "C++17 遍历 tuple: ";
print_tuple_cxx17(t);
std::cout << "C++20 遍历 tuple: ";
print_tuple_cxx20(t);
#if __cplusplus >= 202600L
std::cout << "C++26 遍历 tuple: ";
print_tuple_cxx26(t);
#endif
// 也可以测试 std::array
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << "C++20 遍历 std::array: ";
print_tuple_cxx20(arr); // C++20 版本可以兼容 std::array
return 0;
}
ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
C++17 遍历 tuple: 42, 3.14, x, hello
C++20 遍历 tuple: 42, 3.14, x, hello
C++20 遍历 std::array: 1, 2, 3, 4, 5
https://godbolt.org/z/hxnnhqbox
1⃣ Hello, unroll — 第一版
#include <cstdlib>
#include <iostream>
template <size_t n, typename F>
constexpr auto unroll(F&& f) {
#pragma unroll n
for (size_t i = 0; i < n; ++i) {
f(i);
}
}
int main(){
// 使用方法:
unroll<8>([](size_t i) { std::cout << i; });
}
理解
#pragma unroll n是编译器提示,希望循环展开(unroll)- 循环体内调用
f(i),参数i是运行时变量 - 问题:
- 非标准(非 ISO C++)
- 编译器行为不确定(不同厂商/版本/优化标志可能不同)
- 不能在编译期当作常量使用,例如 tuple 遍历:
std::get<i>(tuple)会报错,因为i不是编译期常量
2⃣ Second Take — 第二版(C++17 fold expression + index_sequence)
#include <cstdlib>
#include <iostream>
template <size_t n, typename F, size_t... i>
constexpr void unroll(F&& f, std::index_sequence<i...> = {}) {
if constexpr (sizeof...(i) != n) {
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
(f(i), ...); // fold expression 展开
}
}
int main(){
// 使用方法:
auto t = std::tuple{1, "two"};
unroll<2>([&](auto i) { std::cout << i; }); // OK
// unroll<2>([&](auto i) { std::cout << std::get<i>(t); }); // ERROR!
}
理解
- 使用
std::index_sequence生成索引 pack - fold expression
(f(i), ...)展开执行函数 - 问题:
- 虽然可以遍历索引,但 i 仍然是模板参数 pack 的普通类型,不是
std::integral_constant - 所以仍然不能用于 编译期上下文(如
std::get<i>(t)需要常量模板参数)
- 虽然可以遍历索引,但 i 仍然是模板参数 pack 的普通类型,不是
3⃣ Working unroll — 第三版(用 std::integral_constant 解决编译期索引问题)
#include <tuple>
#include <iostream>
#include <utility> // for std::index_sequence
template <size_t n, typename F, size_t... i>
constexpr void unroll(F&& f, std::index_sequence<i...> = {}) {
if constexpr (sizeof...(i) != n) {
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// 传递 std::integral_constant<size_t, i>,编译期常量
(f(std::integral_constant<size_t, i>()), ...);
}
}
// 使用示例:
int main() {
auto t = std::tuple{1, "two"};
// 访问 i 本身
unroll<2>([&](auto i) { std::cout << i << " "; });
std::cout << "\n";
// 编译期索引访问 tuple
unroll<2>([&](auto i) { std::cout << std::get<i>(t) << " "; });
std::cout << "\n";
return 0;
}
理解
- 核心技巧:把索引
i包装成 编译期常量类型
i → s t d : : i n t e g r a l _ c o n s t a n t < s i z e t , i > i \rightarrow std::integral\_constant<size_t, i> i→std::integral_constant<sizet,i> - 优点:
std::get<i>(tuple)现在可以使用编译期常量i- 不需要额外 helper 函数
- 遍历 tuple、array、或者 pack 都可以
- fold expression:
( f ( s t d : : i n t e g r a l c o n s t a n t < i > ( ) ) , … ) (f(std::integral_constant<i>()), \dots) (f(std::integralconstant<i>()),…)
将所有索引展开成连续调用,类似循环展开(loop unrolling) - 总结:
- 第一版:只用
#pragma unroll,非标准 - 第二版:用
index_sequence展开,仍然不能用作编译期索引 - 第三版:用
std::integral_constant完全解决编译期索引问题 ✓
- 第一版:只用
1⃣ 问题背景:过度迭代(TMI)
假设我们有一个 std::array,想找某个匹配的元素:
#include <array>
#include <iostream>
int main() {
auto a = std::array<int, 5>{1, 5, 2, 4, 3};
auto i = a.size();
// 使用 unroll 遍历每个元素
unroll<a.size()>([&](auto j) {
if (a[j] == 42) i = j;
});
std::cout << "Found index: " << i << "\n";
}
理解
unroll<a.size()>会 强制遍历所有元素,即使找到了匹配元素- 因此如果匹配的是第一个元素,循环仍会继续检查后面的元素
- 返回后必须再次检查
i是否被更新 - 效率低下,尤其是大数组或复杂操作时
这就是所谓的 TMI(Too Much Iteration)。
2⃣ 改进方案:带布尔返回值的 lambda
为了避免多余迭代,可以让 lambda 返回 bool,并在 lambda 返回 false 时提前终止迭代。
template <size_t n, typename F, size_t... i>
constexpr auto unroll(F&& f, std::index_sequence<i...> = {}) {
if constexpr (sizeof...(i) != n) {
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// fold expression 布尔与运算:一旦 lambda 返回 false, fold 停止
return (f(std::integral_constant<size_t, i>()) && ...);
}
}
int main(){
// 使用方法:
auto a = std::array<int, 5>{1, 5, 2, 4, 3};
size_t i = a.size();
if (!unroll<a.size()>([&](auto j) {
return a[j] == 42 ? (i = j, false) : true; // 找到匹配则返回 false 提前停止
})) {
std::cout << "Found at index " << i << "\n";
} else {
std::cout << "Not found\n";
}
}
理解
- fold expression
(f(...) && ...)的特点:一旦中间结果为 false,后续 lambda 不再调用 - lambda 的返回值
bool决定了是否提前退出 (i = j, false)是 C++ 中的逗号表达式:i = j先执行赋值- 返回
false用于终止迭代
- 如果 lambda 返回
true,则继续迭代
3⃣ μidea:根据 lambda 类型决定行为
为了兼容两种场景:
- void lambda → 完整迭代,不提前退出
- bool lambda → 可以提前退出
// void lambda:执行所有迭代
unroll<5>([&](auto i) { std::cout << i << " "; });
// bool lambda:可以提前退出
size_t idx = 0;
if (!unroll<5>([&](auto i) { return i == 3 ? (idx = i, false) : true; })) {
std::cout << "Stopped early at " << idx << "\n";
}
理解
- 通过 lambda 返回类型 introspection(类型判断) 可以自动选择行为
- 如果 lambda 返回
void→ 完整迭代 - 如果 lambda 返回
bool→ 可以提前退出 - 当前 C++ 版本只能用“穷人版 introspection”,也就是用 不同的函数重载 来区分
4⃣ 总结
- 问题:第一版 unroll 过度迭代(TMI)
- 改进:lambda 返回
bool,可提前退出 - 思路:
- 使用
std::integral_constant传递编译期索引 - fold expression
(f(...) && ...)控制提前退出 - 根据 lambda 返回类型决定迭代策略
- 使用
- 应用场景:
- 搜索数组/tuple/pack
- GPU uniform 或 buffer 更新
- 编译期展开循环
1⃣ 代码原型
template <size_t n, typename F, size_t... i>
constexpr auto unroll(F&& f, std::index_sequence<i...> = std::index_sequence<>()) {
if constexpr (sizeof...(i) != n) {
// 如果索引 pack 长度不等于 n,则生成完整索引序列
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// 推断 lambda 返回类型
using result_t = decltype(f(std::integral_constant<size_t, 0>()));
if constexpr (std::is_void_v<result_t>) {
// lambda 返回 void → 完整迭代
return (f(std::integral_constant<size_t, i>()), ...); // fold expression
} else {
// lambda 返回 bool → 遇到 false 提前退出
return (f(std::integral_constant<size_t, i>()) && ...); // fold expression
}
}
}
2⃣ 逐行理解
2.1 模板参数
template <size_t n, typename F, size_t... i>
n:编译期常量,表示循环次数F:lambda 或可调用对象i...:索引 pack,用于展开循环
2.2 默认索引序列
std::index_sequence<i...> = std::index_sequence<>()
- 提供默认值,便于第一次调用时没有索引 pack
- 后续通过
std::make_index_sequence<n>()自动生成[0, 1, ..., n-1]
2.3 生成索引序列
if constexpr (sizeof...(i) != n) {
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
}
sizeof...(i):当前 pack 的长度- 如果索引数量不够,生成完整索引序列递归调用自身
- 这是一个 编译期递归展开技巧
2.4 推断 lambda 返回类型
using result_t = decltype(f(std::integral_constant<size_t, 0>()));
- 假设 lambda 至少接收一个索引参数
- 使用
std::integral_constant<size_t, 0>()测试返回类型 - 这样就可以 根据返回类型决定迭代策略
2.5 根据返回类型执行迭代
if constexpr (std::is_void_v<result_t>) {
return (f(std::integral_constant<size_t, i>()), ...);
} else {
return (f(std::integral_constant<size_t, i>()) && ...);
}
2.5.1 lambda 返回 void:
(f(...), ...)是 fold expression- 会完整展开所有
i,不会提前退出 - 类似传统循环
for (i = 0; i < n; i++) f(i);
2.5.2 lambda 返回 bool:
(f(...) && ...)fold expression 的特点:- 一旦 lambda 返回
false,后续迭代停止
- 一旦 lambda 返回
- 这样就实现了 早退出 功能
3⃣ 使用示例
3.1 完整迭代(void lambda)
unroll<5>([](auto i){
std::cout << i << " ";
});
// 输出:0 1 2 3 4
3.2 可提前退出(bool lambda)
size_t found = 0;
bool result = unroll<5>([&](auto i) -> bool {
if (i == 3) { found = i; return false; } // 找到目标提前退出
return true;
});
// 输出:found = 3
4⃣ 核心思想总结
- 索引序列展开:使用
std::index_sequence和 fold expression 展开循环 - 根据 lambda 返回类型选择行为:
void→ 完整迭代bool→ 遇到false提前退出
- 编译期展开:所有循环索引在编译期确定,运行时无循环开销
- 适用场景:
- 编译期遍历 tuple / array / pack
- GPU buffer/uniform 数据上传
- 高性能模板元编程
数学/模板概念公式化
- fold expression 展开可以写作:
( f ( i 0 ) , f ( i 1 ) , . . . , f ( i n − 1 ) ) (void lambda) (f(i_0), f(i_1), ..., f(i_{n-1})) \quad \text{(void lambda)} (f(i0),f(i1),...,f(in−1))(void lambda)
( f ( i 0 ) ∧ f ( i 1 ) ∧ . . . ∧ f ( i n − 1 ) ) (bool lambda) (f(i_0) \land f(i_1) \land ... \land f(i_{n-1})) \quad \text{(bool lambda)} (f(i0)∧f(i1)∧...∧f(in−1))(bool lambda)
#include <array>
#include <iostream>
#include <utility> // for std::index_sequence
template <size_t n, typename F, size_t... i>
constexpr auto unroll(F&& f, std::index_sequence<i...> = std::index_sequence<>()) {
if constexpr (sizeof...(i) != n) {
// 如果索引 pack 长度不等于 n,则生成完整索引序列
return unroll<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// 推断 lambda 返回类型
using result_t = decltype(f(std::integral_constant<size_t, 0>()));
if constexpr (std::is_void_v<result_t>) {
// lambda 返回 void → 完整迭代
return (f(std::integral_constant<size_t, i>()), ...); // fold expression
} else {
// lambda 返回 bool → 遇到 false 提前退出
return (f(std::integral_constant<size_t, i>()) && ...); // fold expression
}
}
}
int main() {
// void lambda:执行所有迭代
unroll<5>([&](auto i) { std::cout << i << " "; });
// bool lambda:可以提前退出
std::cout<<std::endl;
size_t idx = 0;
if (!unroll<5>([&](auto i) { return i == 3 ? (idx = i, false) : true; })) {
std::cout << "Stopped early at " << idx << "\n";
}
}
https://wandbox.org/permlink/kmvefdn9ys7V2fYm
1⃣ Filtering make_tuple
核心思想
- 我们希望在构建 tuple 时 过滤掉某些元素(比如占位符
null_field) - 传统方法效率低:先构建 tuple 再过滤
- 解决方案:在生成 tuple 时就过滤
代码示例
// 空字段占位类型
struct null_field {};
// 自定义 make_tuple,可自动忽略 null_field
template <typename... Ts>
auto make_tuple([[maybe_unused]] Ts&&... vs) {
auto one_or_none = []([[maybe_unused]] auto&& x) {
if constexpr (std::is_convertible_v<decltype(x), const null_field&>) {
// 如果是 null_field → 返回空 tuple
return std::make_tuple();
} else {
// 正常元素 → 返回单元素 tuple
return std::make_tuple(std::forward<decltype(x)>(x));
}
};
// 将所有单元素 tuple 拼接成最终 tuple
return std::tuple_cat(one_or_none(std::forward<Ts>(vs))...);
}
// 使用示例
auto t = make_tuple(1, null_field(), 2); // 等价于 std::make_tuple(1, 2)
注释理解:
null_field→ 充当“不可用/忽略”类型one_or_nonelambda → 根据类型决定是否加入 tuplestd::tuple_cat→ 将多个 tuple 拼接成最终 tuple
2⃣ Unroll 到 Tuple
核心思想
- 类似前面
unroll,但我们希望将 lambda 的结果展开成 tuple - 配合上面的
make_tuple,可以直接过滤掉 null_field
代码示例
template <size_t n, typename F, size_t... i>
constexpr auto unroll_to_tuple(F&& f, std::index_sequence<i...> = {}) {
if constexpr (sizeof...(i) != n) {
return unroll_to_tuple<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// 调用自定义 make_tuple,自动过滤 null_field
return make_tuple(f(std::integral_constant<size_t, i>())...);
}
}
使用示例:删除 tuple 的第一个元素
auto t = std::tuple{"meh", 1, 2};
auto u = unroll_to_tuple<3>([&](auto i) {
if constexpr (i > 0) return std::get<i>(t);
else return null_field();
});
// u == std::tuple{1, 2}
使用示例:在第二个位置插入元素
auto t = std::tuple{3.14, 1, 2};
auto u = unroll_to_tuple<4>([&](auto i) {
if constexpr (i != 1) return std::get<i - (i > 1)>(t); // 偏移调整
else return 42; // 插入值
});
assert(u == std::make_tuple(3.14, 42, 1, 2));
3⃣ 遍历 tuple 或 array
核心思想
- 可以用
unroll遍历 tuple 或 array - lambda 可以选择:
- 接收元素索引 + 元素
- 只接收元素
遍历 tuple 示例
template <typename Tuple, typename F>
constexpr void each_in_tuple(Tuple&& t, F&& f) {
constexpr size_t n = std::tuple_size_v<std::remove_reference_t<Tuple>>;
return unroll<n>([&](auto i) {
if constexpr (std::is_invocable_v<F, decltype(i),
decltype(std::get<i>(std::forward<Tuple>(t)))>) {
return f(i, std::get<i>(std::forward<Tuple>(t)));
} else {
return f(std::get<i>(std::forward<Tuple>(t)));
}
});
}
- 可以遍历 tuple 的每个元素
- lambda 可选择是否使用索引
- 支持早退出(bool lambda)
遍历 array 示例
std::array<int, 3> a{1, 2, 3};
each_in_tuple(a, [](auto i, auto val) { std::cout << "a[" << i << "]=" << val << "\n"; });
4⃣ 数组转换为 tuple
auto a = std::array{1, 2, 3};
auto t = unroll_to_tuple<3>([&](auto i) { return a[i]; });
// t == std::tuple{1, 2, 3}
- 将 array 元素展开成 tuple
- 可配合
null_field过滤
5⃣ 小向量(small_vector)理念
- 当元素较少时,使用编译期 unroll 全展开
- 元素稍多时,使用普通迭代
- 例子:NVIDIA 的 small_vector
- 可以高效管理小数组/向量
6⃣ 核心公式化
tuple 拼接/过滤
make_tuple ( x 1 , null_field , x 3 ) = ( x 1 , x 3 ) \text{make\_tuple}(x_1, \text{null\_field}, x_3) = (x_1, x_3) make_tuple(x1,null_field,x3)=(x1,x3)
unroll_to_tuple 展开
unroll_to_tuple < n > ( f ) = make_tuple ( f ( 0 ) , f ( 1 ) , . . . , f ( n − 1 ) ) \text{unroll\_to\_tuple}<n>(f) = \text{make\_tuple}(f(0), f(1), ..., f(n-1)) unroll_to_tuple<n>(f)=make_tuple(f(0),f(1),...,f(n−1))
遍历 tuple
each_in_tuple ( t , f ) = f ( get<0>(t) ) , f ( get<1>(t) ) , . . . , f ( get<n-1>(t) ) \text{each\_in\_tuple}(t, f) = f(\text{get<0>(t)}), f(\text{get<1>(t)}), ..., f(\text{get<n-1>(t)}) each_in_tuple(t,f)=f(get<0>(t)),f(get<1>(t)),...,f(get<n-1>(t))
- 如果 lambda 返回 bool → 遇到 false 提前退出
总结理解
null_field+ 自定义make_tuple→ 过滤 tuple 元素unroll_to_tuple→ 编译期展开索引,生成 tupleeach_in_tuple→ 高度灵活的 tuple/array 遍历,可早退出- 编译期展开 → 高性能,适用于 small_vector、GPU buffer、元编程
#include <iostream>
#include <tuple>
#include <array>
#include <utility>
#include <type_traits>
// =======================
// 1⃣ null_field + make_tuple
// =======================
// null_field 用作“占位/无效元素”,用于在构造 tuple 时忽略某些元素
struct null_field {};
// make_tuple:自定义版本,可自动过滤 null_field 元素
template <typename... Ts>
auto make_tuple(Ts&&... vs) {
// one_or_none: 对每个元素进行处理
// 如果是 null_field 类型,则返回空 tuple,否则返回包含该元素的 tuple
auto one_or_none = [](auto&& x) {
if constexpr (std::is_convertible_v<decltype(x), const null_field&>) {
// x 可转换为 null_field → 返回空 tuple
return std::make_tuple();
} else {
// 否则返回包含 x 的 tuple
return std::make_tuple(std::forward<decltype(x)>(x));
}
};
// tuple_cat 将所有子 tuple 拼接成一个完整的 tuple
return std::tuple_cat(one_or_none(std::forward<Ts>(vs))...);
}
// =======================
// 2⃣ unroll_to_tuple
// =======================
// 通过编译期展开生成 tuple,每个元素由 lambda f 决定
// n: 元素数量
// F: lambda
// i...: 索引展开 pack
template <size_t n, typename F, size_t... i>
constexpr auto unroll_to_tuple(F&& f, std::index_sequence<i...> = {}) {
if constexpr (sizeof...(i) != n) {
// 还没有生成完整的索引序列 → 生成 index_sequence 并递归调用
return unroll_to_tuple<n>(std::forward<F>(f), std::make_index_sequence<n>());
} else {
// 已生成完整序列 → 调用 lambda,并通过 make_tuple 生成最终 tuple
// 注意使用 std::integral_constant<size_t, i> 传递索引 i
return make_tuple(f(std::integral_constant<size_t, i>{})...);
}
}
// =======================
// 3⃣ each_in_tuple (修正版本)
// =======================
// 遍历 tuple 元素,对每个元素执行 lambda f
// 如果 lambda 接受索引和元素,则传入 (i, value),否则仅传入 value
// 内部实现:展开索引 pack
template <typename Tuple, typename F, std::size_t... I>
constexpr void each_in_tuple_impl(Tuple&& t, F&& f, std::index_sequence<I...>) {
(([&] {
// 判断 lambda 是否可调用 f(i, value)
if constexpr (std::is_invocable_v<F, std::integral_constant<size_t, I>, decltype(std::get<I>(t))>) {
f(std::integral_constant<size_t, I>{}, std::get<I>(t)); // 传入索引和值
} else {
f(std::get<I>(t)); // 仅传入值
}
}()), ...); // 使用折叠表达式展开
}
// 外部接口:自动生成 index_sequence
template <typename Tuple, typename F>
constexpr void each_in_tuple(Tuple&& t, F&& f) {
constexpr size_t n = std::tuple_size_v<std::remove_reference_t<Tuple>>; // tuple 元素数量
each_in_tuple_impl(std::forward<Tuple>(t), std::forward<F>(f), std::make_index_sequence<n>{});
}
// =======================
// 4⃣ 测试 main
// =======================
int main() {
std::cout << "--- make_tuple + null_field ---\n";
auto t1 = make_tuple(1, null_field(), 2, 3, null_field());
// 输出 tuple 元素数量(null_field 被过滤掉)
std::cout << "t1 size: " << std::tuple_size_v<decltype(t1)> << "\n"; // 3
std::cout << "--- unroll_to_tuple: remove first element ---\n";
auto t2 = std::tuple{"meh", 1, 2};
// 利用 unroll_to_tuple 删除 tuple 的第一个元素
auto u2 = unroll_to_tuple<3>([&](auto i) {
if constexpr (i > 0) return std::get<i>(t2); // 保留 i>0 的元素
else return null_field{}; // i=0 → 返回 null_field,被过滤
});
std::cout << "u2 size: " << std::tuple_size_v<decltype(u2)> << "\n"; // 2
std::cout << "--- unroll_to_tuple: insert 42 at position 1 ---\n";
auto t3 = std::tuple{3.14, 1, 2};
auto u3 = unroll_to_tuple<4>([&](auto i) {
if constexpr (i != 1) return std::get<i - (i > 1)>(t3); // 原 tuple 元素平移
else return 42; // 在位置 1 插入 42
});
// 输出 tuple
std::cout << "u3: ("
<< std::get<0>(u3) << ", "
<< std::get<1>(u3) << ", "
<< std::get<2>(u3) << ", "
<< std::get<3>(u3) << ")\n";
std::cout << "--- each_in_tuple ---\n";
auto t4 = std::tuple{"apple", "banana", "cherry"};
// 遍历 tuple,lambda 接受索引和值
each_in_tuple(t4, [](auto i, auto val){
std::cout << "t4[" << i << "] = " << val << "\n";
});
std::cout << "--- array -> tuple ---\n";
std::array<int, 3> a{10, 20, 30};
// 将 array 转换为 tuple
auto t5 = unroll_to_tuple<3>([&](auto i){ return a[i]; });
each_in_tuple(t5, [](auto i, auto val){
std::cout << "t5[" << i << "] = " << val << "\n";
});
return 0;
}
注释说明()
- null_field + make_tuple
- 用于在 tuple 构造时过滤掉无效元素,类似数学中的“空元素”。
tuple_cat拼接各子 tuple。
- unroll_to_tuple
- 通过索引展开
(i...)生成 tuple,每个元素由 lambda 返回。 - 使用
std::integral_constant<size_t, i>可以让索引在编译期成为常量。
- 通过索引展开
- each_in_tuple
- 支持 lambda 可选索引参数。
- 使用
std::index_sequence+ fold expression 展开 tuple 元素。
- 用例
- 删除元素、插入元素、tuple 遍历、array 转 tuple。
- 可直接用于元编程优化场景,如 small_vector、固定大小数组操作。
https://wandbox.org/permlink/HRE6EwgbKliOGdNG
概念理解:Partial Unrolling(部分展开)
1⃣ 基本想法
- 已知一个 动态循环次数 m m m,但我们希望 编译期展开(unroll)每次 n n n 次。
- 实现方法:
- 对循环中每 n n n 个元素做展开(inline)。
- 对剩余 r = m m o d n r = m \bmod n r=mmodn 个元素做单独处理。
- 优点:
- 编译期展开 → 避免循环开销。
- GPU 代码中尤其重要(减少循环控制依赖,提升吞吐量)。
数学表示:
for i ∈ [ 0 , m ) : f ( i ) ≈ unroll chunks of size n + process leftovers \text{for } i \in [0, m): \quad f(i) \approx \text{unroll chunks of size } n + \text{process leftovers} for i∈[0,m):f(i)≈unroll chunks of size n+process leftovers
2⃣ Take One:直接展开剩余元素
template <size_t n, typename F>
constexpr void unroll(size_t m, F&& f) {
size_t i = 0;
for (;;) {
size_t j = i + n;
if (j > m) break; // 超过总长度则停止
unroll<n>([&](auto k) { f(i + k); }); // 每块 n 个元素展开
i = j;
}
// 展开剩余元素
unroll<n>([&](auto k) { return (i + k < m) && (f(i + k), true); });
}
问题:
- 对每个剩余元素都做一次测试
(i+k < m)→ 对 GPU 或小循环特别慢。 - 每个线程执行多次条件判断 → 依赖链长,性能下降。
3⃣ Take Two:二分法优化剩余元素展开
思路:
- 用 二分法 减少判断次数:
- 对 16 个元素:
- 测试是否 ≥8 → 如果是,展开 8 个。
- 测试是否 ≥4 → 展开 4 个。
- 测试是否 ≥2 → 展开 2 个。
- 测试是否 ≥1 → 展开 1 个。
- 对 16 个元素:
- 测试次数变为 log 2 n \log_2 n log2n 而非 n n n。
示例:
template <size_t B, size_t E, typename F, size_t... i>
void unroll_leftovers(size_t base, size_t n, F&& f, std::index_sequence<i...> = {}) {
if constexpr (E - B <= 1) {
static_assert(E - B == 1);
if (n) f(base + B);
} else {
constexpr auto M = (E - B) / 2;
if constexpr (sizeof...(i) != M) {
unroll_leftovers<B, E>(base, n, f, std::make_index_sequence<M>());
} else {
if (n >= M) {
(f(base + i + B), ...); // fold expression 展开 M 个元素
unroll_leftovers<B + M, E>(base, n - M, std::forward<F>(f));
} else {
unroll_leftovers<B, E - M>(base, n, std::forward<F>(f));
}
}
}
}
问题:
- 虽然每条路径测试少了,但代码量暴增:
- 对 n=16 → 128 次 lambda 调用。
- GPU 或编译器仍然可能生成大量基本块,增加指令压力。
4⃣ Perfect Take:每个长度只展开一次
核心思想:
- 使用 指数折半 + fold expression:
- 先处理一半元素。
- 再处理剩下元素。
- 每个长度只展开一次,避免重复 lambda 调用。
- 实现:
template <size_t n, typename F, size_t... i>
void unroll_leftovers(size_t base, size_t m, F&& f, std::index_sequence<i...> = {}) {
if constexpr (n > 0) {
if constexpr (sizeof...(i) != n / 2) {
// 递归生成半长 index_sequence
unroll_leftovers<n>(base, m, f, std::make_index_sequence<n / 2>());
} else {
auto m2 = m >= n / 2 ? m - n / 2 : m;
unroll_leftovers<n / 2>(base, m2, std::forward<F>(f)); // 展开前半部分
if (m >= n / 2) {
(f(base + m2 + i), ...); // 展开后半部分
}
}
}
}
特点:
- 使用折叠表达式
(f(...), ...)展开所有元素。 - 避免重复 lambda 调用。
- 每个剩余长度只展开一次 → 最优编译期展开。
- 保证生成代码最小化,同时满足 log(n) 测试优化。
总结 µideas
- 部分展开策略:
- 大块用固定长度 n 展开。
- 小块用二分折半展开,减少条件判断次数。
- 使用 C++ 技巧:
std::index_sequence和std::integral_constant<size_t, i>。- 折叠表达式
(f(...), ...)。 - Lambda 捕获索引和 base。
- null type pattern:
- 可用
null_field等占位类型进行过滤。
- 可用
- 早期退出支持:
- Lambda 可返回 bool 决定是否继续。
- 适合 GPU / 高性能内核:
- 减少循环控制依赖。
- 减少 branch divergence。
总结数学公式化
- 假设总循环次数为 m m m,展开块大小 n n n,剩余元素 r = m m o d n r = m \bmod n r=mmodn。
- 部分展开策略:
for i = 0 to m − n step n : f ( i . . i + n − 1 ) \text{for } i=0 \text{ to } m-n \text{ step } n: \quad f(i..i+n-1) for i=0 to m−n step n:f(i..i+n−1) - 剩余元素:
bisection unfold r elements: r ≤ n \text{bisection unfold } r \text{ elements: } r \le n bisection unfold r elements: r≤n - 测试次数 ≤ log 2 n \le \log_2 n ≤log2n,而非 r r r。
#include <iostream>
#include <utility>
#include <type_traits>
// =======================
// 1⃣ 完整 Partial Unroll 实现
// =======================
// -----------------------
// 1.1 全展开 n 个元素
template <size_t n, typename F, size_t... i>
constexpr void unroll_full(F&& f, std::index_sequence<i...> = {}) {
// fold expression 展开 n 个元素
(f(std::integral_constant<size_t, i>{}), ...);
}
// -----------------------
// 1.2 剩余元素展开(bisection 二分法)
// n: 块大小, base: 起始索引, m: 剩余元素数量
template <size_t n, typename F, size_t... i>
void unroll_leftovers(size_t base, size_t m, F&& f, std::index_sequence<i...> = {}) {
if constexpr (n > 0) {
if constexpr (sizeof...(i) != n / 2) {
// 递归生成 index_sequence 长度 n/2
unroll_leftovers<n>(base, m, f, std::make_index_sequence<n / 2>());
} else {
// 处理剩余的前半部分
auto m2 = m >= n / 2 ? m - n / 2 : m;
unroll_leftovers<n / 2>(base, m2, std::forward<F>(f));
// 如果剩余元素 ≥ n/2,展开后半部分
if (m >= n / 2) {
(f(base + m2 + i), ...);
}
}
}
}
// -----------------------
// 1.3 Partial Unroll 接口
template <size_t n, typename F>
void partial_unroll(size_t m, F&& f) {
size_t i = 0;
// 处理完整块
for (;;) {
size_t j = i + n;
if (j > m) break;
unroll_full<n>([&](auto k) { f(i + k); });
i = j;
}
// 处理剩余元素
unroll_leftovers<n>(i, m - i, [&](auto k){ f(i + k); });
}
// =======================
// 2⃣ 测试 main
// =======================
int main() {
std::cout << "--- Partial Unroll 示例 ---\n";
size_t total = 17; // 总循环次数
constexpr size_t block = 8; // 每块展开大小
std::cout << "total = " << total << ", block size = " << block << "\n";
partial_unroll<block>(total, [&](auto idx){
// idx 为当前元素索引
std::cout << "processing element " << idx << "\n";
});
std::cout << "\n--- 测试 Lambda 早期退出 ---\n";
size_t stop_after = 5;
partial_unroll<block>(total, [&](auto idx){
std::cout << idx << " ";
if (idx + 1 >= stop_after) {
// 这里直接退出程序模拟 early exit
std::cout << "(early exit)\n";
std::exit(0);
}
});
return 0;
}
代码理解
unroll_full:- 使用 fold expression
(f(...), ...)展开固定长度n。 std::integral_constant<size_t, i>用作编译期索引。
- 使用 fold expression
unroll_leftovers:- 用二分法展开剩余元素。
- 将长度折半递归展开,避免重复条件测试。
- 每个剩余长度只展开一次 → 最优展开。
partial_unroll:- 先用 for 循环处理完整块。
- 再用
unroll_leftovers展开剩余元素。
- Lambda 支持早期退出:
- 可以在 Lambda 内部使用
return或std::exit控制。 - 避免循环浪费。
✓ 特点:
- 可以在 Lambda 内部使用
- 完全 C++20 可编译。
- 适合 GPU / 高性能场景。
- 展开代码量控制合理。
- 支持动态总次数
m+ 编译期固定块n。 - 支持 索引访问。
https://wandbox.org/permlink/TsI0qszyYL59VtrN
更多推荐
所有评论(0)