如何在 C++11 (STL) 中创建一个压缩两个元组的函数?

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

I recently ran across this puzzle, was finally able to struggle out a hacky answer (using index arrays), and wanted to share it (answer below). I am sure there are answers that use template recursion and answers that use boost; if you're interested, please share other ways to do this. I think having these all in one place may benefit others and be useful for learning some of the cool C++11 template metaprogramming tricks.

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

Problem: Given two tuples of equal length:

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

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

How do you create a function that will "zip" the two tuples into a heterogeneous tuple of pairs?

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?!"));

推荐答案

首先快速了解一下索引数组:

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 函数:

And now to use our print_helper function:

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

键入 seq<0,1,2> 可能有点痛苦.所以我们可以使用模板递归创建一个类来生成seqs,这样gens<3>::type就和seq<0,1一样,2>:

Typing seq<0,1,2> can be a bit of a pain, though. So we can use template recursion to create a class to generate seqs, so that gens<3>::type is the same as seq<0,1,2>:

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<N>::type 中的 N 将始终是元组中元素的数量,因此您可以包装 print_helper为了更容易:

Since the N in gens<N>::type will always be the number of elements in the tuple, you can wrap print_helper to make it easier:

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;
}

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

Note that the template arguments can be deduced automatically (typing all of that out would be a pain wouldn't it?).

现在,tuple_zip 函数:

Now, the tuple_zip function:

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

As before, start with the helper function:

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,并在定义参数后提供->).这让我们避免了定义返回类型的问题,只需声明它返回函数体中使用的表达式(如果 xyintdelctype(x+y) 在编译时被解析为 int).

The code is a little tricky, particularly the trailing return type (the return type is declared as auto and provided with -> after the parameters are defined). This lets us avoid the problem of even defining what the return type will be, by simply declaring it returns the expression used in the function body (if x and y are ints, delctype(x+y) is resolved at compile time as int).

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

Now wrap it in a function that provides the appropriate seq<0, 1...N> using gens<N>::type:

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() );
}

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

Now you can use it as specified in the question:

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 提供 << 运算符,您可以使用我们上面定义的打印函数来打印压缩结果:

And finally, if you provide a << operator for std::pair you can use the print function we defined above to print the zipped result:

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?!)

zipping: 1 b 10
with : 2.5 2 even strings?!
yields : pair(1,2.5) pair(b,2) pair(10,even strings?!)

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

Like std::array, std::tuple is defined at compile time, and so it can be used to generate more optimizable code (more information is known at compile time compared to containers like std::vector and std::list). So even though it's sometimes a bit of work, you can sometimes use it to make fast and clever code. Happy hacking!

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

As requested, allowing tuples of different sizes and padding with null pointers:

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 功能:

And BTW, you're going to need this now to use our handy print function:

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

相关文章