如何延迟展开C++参数包?元组zip场景下的技术问询
实现任意数量std::tuple的zip操作:延迟展开参数包的优雅方案
我正在研究std::tuple的用法,想实现任意数量元组的zip操作。目前的解决方案不够理想,过程中碰到一个技术难点:
有时候需要分别展开参数包,但这些参数包会同时作为模板参数和函数参数出现在同一个调用里。我找不到一种直观的方法,能单独展开Is参数包而不展开ts,还不用先把ts打包成std::tuple再用std::get<I>解包。
显然把一个函数拆成五个不是好办法,我知道可以用lambda,但不确定会不会更简洁。有没有优雅的方式延迟展开ts参数包?
原代码示例
#include <iostream> #include <tuple> #include <string_view> template <typename T, typename... Ts> auto get_first(T t, Ts...) { return t; } template <size_t I, typename... Ts> auto zip2_impl4(Ts... ts) { return std::make_tuple(std::get<I>(ts)...); } template <size_t I, typename T, size_t... Is> auto zip2_impl3(T t, std::index_sequence<Is...>) { return zip2_impl4<I>(std::get<Is>(t)...); } template <size_t I, typename T> auto zip2_impl2(T t) { using size = std::tuple_size<T>; using seq = std::make_index_sequence<size::value>; return zip2_impl3<I>(t, seq{}); } template <size_t... Is, typename... Ts> auto zip2_impl(std::index_sequence<Is...> seq, Ts... ts) { // need to defer expanding the pack ts, // because the packs Is and ts need to expand separately auto t = std::make_tuple(ts...); return std::make_tuple(zip2_impl2<Is>(t)...); } template <typename... Ts> auto zip2(Ts... ts) { using size = std::tuple_size<decltype(get_first(ts...))>; using seq = std::make_index_sequence<size::value>; return zip2_impl(seq{}, ts...); } int main() { using namespace std::literals; auto ints = std::make_tuple(1,2,3); auto svs1 = std::make_tuple("a"sv, "b"sv, "c"sv); auto svs2 = std::make_tuple("d"sv, "e"sv, "f"sv); auto zipped = zip2(ints, svs1, svs2); std::apply([](auto... args) { (std::apply([](auto... args) { ((std::cout << args), ...); }, args), ...); }, zipped); return 0; }
原代码输出
1ad2be3cf
优雅的解决方案
我们可以利用C++17的折叠表达式和模板参数推导,直接简化逻辑,避免多层辅助函数,同时完美实现参数包的延迟展开:
#include <iostream> #include <tuple> #include <string_view> #include <type_traits> template <size_t... Is, typename... Ts> auto zip_impl(std::index_sequence<Is...>, Ts&&... ts) { // 外层展开Is参数包,内层对每个Is展开ts参数包 return std::make_tuple( std::make_tuple(std::get<Is>(std::forward<Ts>(ts))...) ... ); } template <typename... Ts> auto zip(Ts&&... ts) { // 取第一个元组的大小作为zip后的元素数量 using FirstTuple = std::decay_t<decltype((ts, ...))>; constexpr size_t TupleSize = std::tuple_size_v<FirstTuple>; return zip_impl(std::make_index_sequence<TupleSize>{}, std::forward<Ts>(ts)...); } int main() { using namespace std::literals; auto ints = std::make_tuple(1,2,3); auto svs1 = std::make_tuple("a"sv, "b"sv, "c"sv); auto svs2 = std::make_tuple("d"sv, "e"sv, "f"sv); auto zipped = zip(ints, svs1, svs2); std::apply([](auto... args) { (std::apply([](auto... elems) { ((std::cout << elems), ...); }, args), ...); }, zipped); return 0; }
方案解析
- 延迟展开的核心逻辑:在
zip_impl中,外层的...会逐个遍历Is中的每个索引;对每个索引Is,内层的...会遍历所有元组ts,取出每个元组中第Is个元素组成新元组。这就实现了先固定Is、再展开ts的需求,完全不需要额外把ts打包成临时元组。 - 简化结构:去掉了原代码中多层嵌套的辅助函数,只用两个函数就完成了所有逻辑,代码可读性更高。
- 性能优化:使用
std::forward完美转发参数,避免不必要的拷贝操作。
优化后输出
1ad2be3cf
内容的提问来源于stack exchange,提问作者okovko
相关产品推荐
相关产品推荐

