【问题标题】:How to make a function that zips two tuples in C++11 (STL)?如何在 C++11 (STL) 中创建一个压缩两个元组的函数?
【发布时间】:2012-07-04 13:04:52
【问题描述】:

我最近遇到了这个难题,终于能够找到一个 hacky 答案(使用索引数组),并想分享它(答案如下)。我确信有使用模板递归的答案和使用boost 的答案;如果您有兴趣,请分享其他方法来做到这一点。我认为将这些全部放在一个地方可能会使其他人受益,并且对于学习一些很酷的 C++11 模板元编程技巧很有用。

问题: 给定两个长度相等的元组:

auto tup1 = std::make_tuple(1, 'b', -10);
auto tup2 = std::make_tuple(2.5, 2, std::string("even strings?!"));

如何创建将两个元组“压缩”成对的异构元组的函数?

std::tuple<
    std::pair<int, double>,
    std::pair<char, int>,
    std::pair<int, std::string> > result =
    tuple_zip( tup1, tup2 );

在哪里

std::get<0>(result) == std::make_pair(1, 2.5);
std::get<1>(result) == std::make_pair('b', 2);
std::get<2>(result) == std::make_pair(-10, std::string("even strings?!"));

【问题讨论】:

  • 现在还有额外的好处:1/ 你能不能让它适用于长度不等的元组(例如用nullptr_t 填充)?以及更多的参与 2/ 你可以让它与任意数量的元组一起工作吗?
  • 压缩两个元组是什么意思?你能发布预期的结果吗?
  • @BЈовић 我已将最终main 例程的所需输出添加到下面的答案中。通过 zip,我的意思是取上面的两个元组(tup1tup2)并在这种情况下创建一个包含 3 个元素的元组,其中第一个是 make_pair(1, 2.5),等等。
  • @MatthieuM。瞧!下面的编辑具有不同大小的元组的填充。 :)

标签: c++ c++11 tuples template-meta-programming


【解决方案1】:

首先简要介绍一下索引数组:

template<std::size_t ...S>
struct seq { };

// And now an example of how index arrays are used to print a tuple:
template <typename ...T, std::size_t ...S>
void print_helper(std::tuple<T...> tup, seq<S...> s) {
  // this trick is exceptionally useful:
  // ((std::cout << std::get<S>(tup) << " "), 0) executes the cout
  // and returns 0.
  // { 0... } expands (because the expression has an S in it),
  // returning an array of length sizeof...(S) full of zeros.
  // The array isn't used, but it's a great hack to do one operation
  // for each std::size_t in S.
  int garbage[] = { ((std::cout << std::get<S>(tup) << " "), 0)... };
  std::cout << std::endl;
}

现在使用我们的 print_helper 函数:

int main() {
  print_helper(std::make_tuple(10, 0.66, 'h'), seq<0,1,2>() );
  return 0;
}

不过,输入 seq&lt;0,1,2&gt; 可能会有点痛苦。所以我们可以使用模板递归创建一个类来生成seqs,这样gens&lt;3&gt;::type就和seq&lt;0,1,2&gt;一样:

template<std::size_t N, std::size_t ...S>
struct gens : gens<N-1, N-1, S...> { };

template<std::size_t ...S>
struct gens<0, S...> {
  typedef seq<S...> type;
};

int main() {
  print_helper(std::make_tuple(10, 0.66, 'h'), gens<3>::type() );
  return 0;
}

由于gens&lt;N&gt;::type 中的N 始终是元组中元素的数量,因此您可以将print_helper 包装起来更方便:

template <typename ...T>
void print(std::tuple<T...> tup) {
  print_helper(tup, typename gens<sizeof...(T)>::type() );
}

int main() {
  print(std::make_tuple(10, 0.66, 'h'));
  return 0;
}

请注意,模板参数可以自动推导出(输入所有这些会很痛苦,不是吗?)。

现在,tuple_zip 函数:

和以前一样,从辅助函数开始:

template <template <typename ...> class Tup1,
    template <typename ...> class Tup2,
    typename ...A, typename ...B,
    std::size_t ...S>
auto tuple_zip_helper(Tup1<A...> t1, Tup2<B...> t2, seq<S...> s) ->
decltype(std::make_tuple(std::make_pair(std::get<S>(t1),std::get<S>(t2))...)) {
  return std::make_tuple( std::make_pair( std::get<S>(t1), std::get<S>(t2) )...);
}

代码有点棘手,尤其是尾随返回类型(返回类型声明为auto,并在定义参数后提供-&gt;)。这让我们避免了定义返回类型的问题,只需声明它返回函数体中使用的表达式(如果xyints, delctype(x+y) 在编译时被解析为 int)。

现在将其包装在一个函数中,该函数使用gens&lt;N&gt;::type 提供适当的seq&lt;0, 1...N&gt;

template <template <typename ...> class Tup1,
  template <typename ...> class Tup2,
  typename ...A, typename ...B>
auto tuple_zip(Tup1<A...> t1, Tup2<B...> t2) ->
decltype(tuple_zip_helper(t1, t2, typename gens<sizeof...(A)>::type() )) {
  static_assert(sizeof...(A) == sizeof...(B), "The tuple sizes must be the same");
  return tuple_zip_helper( t1, t2, typename gens<sizeof...(A)>::type() );
}

现在您可以按照问题中的说明使用它了:

int main() {
  auto tup1 = std::make_tuple(1, 'b', -10);
  auto tup2 = std::make_tuple(2.5, 2, std::string("even strings?!"));
  std::tuple<
    std::pair<int, double>,
    std::pair<char, int>,
    std::pair<int, std::string> > x = tuple_zip( tup1, tup2 );

  // this is also equivalent:
  //  auto x = tuple_zip( tup1, tup2 );

  return 0;
}

最后,如果您为std::pair 提供&lt;&lt; 运算符,您可以使用我们上面定义的打印函数来打印压缩结果:

template <typename A, typename B>
std::ostream & operator << (std::ostream & os, const std::pair<A, B> & pair) {
  os << "pair("<< pair.first << "," << pair.second << ")";
  return os;
}

int main() {
  auto tup1 = std::make_tuple(1, 'b', -10);
  auto tup2 = std::make_tuple(2.5, 2, std::string("even strings?!"));
  auto x = tuple_zip( tup1, tup2 );

  std::cout << "zipping: ";
  print(tup1);
  std::cout << "with   : ";
  print(tup2);

  std::cout << "yields : ";
  print(x);

  return 0;
}

输出是:

拉链:1 b 10
with : 2.5 2 even strings?!
产生:pair(1,2.5) pair(b,2) pair(10,even strings?!)

std::array 一样,std::tuple 是在编译时定义的,因此它可用于生成更优化的代码(与std::vectorstd::list 等容器相比,在编译时可以获得更多信息)。因此,即使有时需要一些工作,您有时也可以使用它来编写快速而聪明的代码。快乐的黑客攻击!


编辑:

根据要求,允许不同大小的元组和空指针填充:

template <typename T, std::size_t N, std::size_t ...S>
auto array_to_tuple_helper(const std::array<T, N> & arr, seq<S...> s) -> decltype(std::make_tuple(arr[S]...)) {
  return std::make_tuple(arr[S]...);
}

template <typename T, std::size_t N>
auto array_to_tuple(const std::array<T, N> & arr) -> decltype( array_to_tuple_helper(arr, typename gens<N>::type()) ) {
  return array_to_tuple_helper(arr, typename gens<N>::type());
}

template <std::size_t N, template <typename ...> class Tup, typename ...A>
auto pad(Tup<A...> tup) -> decltype(tuple_cat(tup, array_to_tuple(std::array<std::nullptr_t, N>()) )) {
  return tuple_cat(tup, array_to_tuple(std::array<std::nullptr_t, N>()) );
}

#define EXTENSION_TO_FIRST(first,second) ((first)>(second) ? (first)-(second) : 0)

template <template <typename ...> class Tup1, template <typename ...> class Tup2, typename ...A, typename ...B>
auto pad_first(Tup1<A...> t1, Tup2<B...> t2) -> decltype( pad<EXTENSION_TO_FIRST(sizeof...(B), sizeof...(A)), Tup1, A...>(t1) ) {
  return pad<EXTENSION_TO_FIRST(sizeof...(B), sizeof...(A)), Tup1, A...>(t1);
}

template <template <typename ...> class Tup1, template <typename ...> class Tup2, typename ...A, typename ...B>
auto diff_size_tuple_zip(Tup1<A...> t1, Tup2<B...> t2) ->
  decltype( tuple_zip( pad_first(t1, t2), pad_first(t2, t1) ) ) {
  return tuple_zip( pad_first(t1, t2), pad_first(t2, t1) );
}

顺便说一句,你现在需要这个来使用我们方便的print 函数:

std::ostream & operator << (std::ostream & os, std::nullptr_t) {
  os << "null_ptr";
  return os;
}

【讨论】:

  • 不错的整体解决方案。我只会说两句。 1/ 我会使用unsigned 作为索引(至少),因为你不期望负索引,是吗? 2/ 欢迎使用static_assert 而不是assert。 3/ 这对可能应该被const&amp;operator&lt;&lt; 重载中使用。
  • 泛化tuple_zip 操作任意数量的元组作为练习留给读者? :)
  • 对于不相等的,我想我会使用decltype(std::ignore)而不是std::nullptr_t,因为我认为它玩得更好:它允许与std::nullptr_t的实际元组区分开来,并且仍然有效好的引用元组。此外,遗憾的是标准 tuple_cat 不需要使用数组(即使它们存在std::get 重载)强制额外的副本:( 我不得不求助于实现我自己的 tuple_cat。
  • @R.MartinhoFernandes 手指交叉编译器通过复制传播将它们取出...
  • 您的 print_helper 无法打印零大小的元组(不允许零大小的自动数组)。
【解决方案2】:

对任意数量的元组执行此操作并不是非常困难。

一种方法是创建一个函数,将 N 个元组中特定索引处的所有元素收集到一个新元组中。然后有另一个函数将这些元组收集到一个新元组中,用于原始元组中的每个索引。

所有这些都可以通过使用参数包扩展表达式相对简单地完成,无需任何递归函数。

#include <cstddef>
#include <tuple>

namespace detail {
    // Describe the type of a tuple with element I from each input tuple.
    // Needed to preserve the exact types from the input tuples.
    template<std::size_t I, typename... Tuples>
    using zip_tuple_at_index_t = std::tuple<std::tuple_element_t<I, std::decay_t<Tuples>>...>;

    // Collect all elements at index I from all input tuples as a new tuple.
    template<std::size_t I, typename... Tuples>
    zip_tuple_at_index_t<I, Tuples...> zip_tuple_at_index(Tuples && ...tuples) {
        return {std::get<I>(std::forward<Tuples>(tuples))...};
    }

    // Create a tuple with the result of zip_tuple_at_index for each index.
    // The explicit return type prevents flattening into a single tuple
    // when sizeof...(Tuples) == 1 or sizeof...(I) == 1 .
    template<typename... Tuples, std::size_t... I>
    std::tuple<zip_tuple_at_index_t<I, Tuples...>...> tuple_zip_impl(Tuples && ...tuples, std::index_sequence<I...>) {
        return {zip_tuple_at_index<I>(std::forward<Tuples>(tuples)...)...};
    }

}

// Zip a number of tuples together into a tuple of tuples.
// Take the first tuple separately so we can easily get its size.
template<typename Head, typename... Tail>
auto tuple_zip(Head && head, Tail && ...tail) {
    constexpr std::size_t size = std::tuple_size_v<std::decay_t<Head>>;

    static_assert(
        ((std::tuple_size_v<std::decay_t<Tail>> == size) && ...),
        "Tuple size mismatch, can not zip."
    );

    return detail::tuple_zip_impl<Head, Tail...>(
        std::forward<Head>(head),
        std::forward<Tail>(tail)...,
        std::make_index_sequence<size>()
    );
}

在此处查看实际操作:https://wandbox.org/permlink/EQhvLPyRfDrtjDMw

我使用了一些 C++14/17 功能,但没有任何必要。最难替换的部分是用于检查元组大小的折叠表达式。那可能必须成为递归检查。

【讨论】:

    猜你喜欢
    • 2014-03-27
    • 1970-01-01
    • 1970-01-01
    • 2017-11-06
    • 1970-01-01
    • 2012-01-20
    相关资源
    最近更新 更多