Как сделать функцию, которая застегивает два кортежа в С++ 11 (STL)? - программирование

Как сделать функцию, которая застегивает два кортежа в С++ 11 (STL)?

Недавно я столкнулся с этой загадкой, наконец, смог бороться с хакерским ответом (используя индексные массивы) и хотел поделиться им (ответ ниже). Я уверен, что есть ответы, которые используют рекурсию шаблона и ответы, которые используют boost; если вам интересно, пожалуйста, поделитесь другими способами. Я думаю, что все это в одном месте может принести пользу другим и быть полезным для изучения некоторых интересных трюков метапрограммирования шаблонов С++ 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?!"));
4b9b3361

Ответ 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 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<0,1,2> может быть немного больным. Поэтому мы можем использовать рекурсию шаблонов для создания класса для генерации seq s, так что gens<3>::type совпадает с 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;
}

Так как N in gens<N>::type всегда будет числом элементов в кортеже, вы можете обернуть 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 и снабжен -> после определения параметров). Это позволяет нам избежать проблемы даже определения того, каков будет тип возврата, просто объявив, что он возвращает выражение, используемое в теле функции (если x и y являются int s, delctype(x+y) разрешено при компиляции время как int).

Теперь оберните его в функцию, которая предоставляет соответствующий seq<0, 1...N> с помощью 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() );
}

Теперь вы можете использовать его, как указано в вопросе:

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, вы можете использовать указанную выше функцию печати для печати заархивированного результата:

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

Вывод:

zipping: 1 b 10
с: 2.5 2 даже строки!
дает: пара (1,2,5) пара (b, 2) пара (10, четные строки?!)

Подобно std::array, std::tuple определяется во время компиляции, поэтому его можно использовать для создания более оптимизируемого кода (более подробная информация известна во время компиляции по сравнению с контейнерами типа std::vector и std::list). Поэтому, хотя иногда это немного работает, вы иногда можете использовать его для быстрого и умного кода. Счастливый взлом!


Edit:

В соответствии с запросом, позволяя кортежи разных размеров и дополнений с нулевыми указателями:

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

И BTW, вам понадобится это сейчас, чтобы использовать нашу удобную функцию print:

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

Ответ 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

Я использовал некоторые возможности С++ 14/17, но ничего существенного. Самой трудной частью для замены было бы выражение fold для проверки размеров кортежей. Это, вероятно, должно стать рекурсивной проверкой.