将多个元组应用于同一个函数(即没有递归或“tuple_cat”的应用(f, tuple…)

时间:2022-12-21 18:34:29

std::experimental::apply has the following signature:

实验证明:应用具有以下特征:

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t);

It basically invokes f by expanding t's elements as the arguments.

它通过扩展t的元素作为参数来调用f。


I would like something that does the exact same thing, but with multiple tuples at the same time:

我想做同样的事情,但同时有多个元组:

template <class F, class... Tuples>
constexpr decltype(auto) multi_apply(F&& f, Tuples&&... ts);

Example usage:

使用示例:

std::tuple t0{1, 2, 3};
std::tuple t1{4, 5, 6};
auto sum = [](auto... xs){ return (0 + ... + xs); };

assert(multi_apply(sum, t0, t1) == 1 + 2 + 3 + 4 + 5 + 6);

I can think of various naive way of implementing multi_apply:

我可以想出多种简单的方法来实现multi_apply:

  • Use std::tuple_cat and then calling std::experimental::apply.

    使用std::tuple_cat,然后调用std::experimental::apply。

  • Use recursion to bind every tuple's arguments to a series of lambdas that eventually call the original function.

    使用递归将每个tuple的参数绑定到最终调用原始函数的一系列lambdas。

But what I'm asking is: how can I implement multi_apply without resorting to std::tuple_cat or recursion?

但是我要问的是:我如何实现multi_apply而不求助于std::tuple_cat或递归?

What I ideally would like to do is: generate an std::index_sequence for every tuple and match every tuple with its own index sequence in the same variadic expansion. Is this possible? Example:

我理想的做法是:为每个tuple生成一个std::index_sequence,并将每个tuple与它自己的索引序列匹配在相同的变量扩展中。这是可能的吗?例子:

// pseudocode-ish
template <class F, std::size_t... Idxs, class... Tuples>
constexpr decltype(auto) multi_apply_helper(
    F&& f, std::index_sequence<Idxs>... seqs,  Tuples&&... ts)
{
    return f(std::get<Idxs>(ts)...);
} 

1 个解决方案

#1


10  

Here's my take on it. It doesn't use recursion and it expands those tuples in the same pack expansion, but it requires a bit of preparation:

这是我的看法。它不使用递归,它在相同的包扩展中扩展了这些元组,但它需要一点准备:

  • We build a tuple of references to the tuples passed in, rvalue references for rvalue arguments, lvalue references for lvalue arguments, in order to have proper forwarding in the final call (exactly what std::forward_as_tuple does, as T.C. noted in the comments). The tuple is built and passed around as an rvalue, so reference collapsing ensures correct value categories for each argument in the final call to f.
  • 我们为传入的元组构建了一个元组,为rvalue参数提供了rvalue引用,为lvalue参数提供了lvalue引用,以便在最终调用中有适当的转发(std::forward_as_tuple,正如T.C.在评论中提到的那样)。tuple是作为一个rvalue构建和传递的,因此引用崩溃将确保在对f的最后调用中每个参数的正确值类别。
  • We build two flattened index sequences, both of size equal to the sum of all tuple sizes:
    • The outer indices select the tuple, so they repeat the same value (the tuple's index in the tuple pack) a number of times equal to the size of each tuple.
    • 外部索引选择元组,因此它们重复相同的值(tuple包中的元组索引),其数量等于每个元组的大小。
    • The inner ones select the element in each tuple, so they increase from 0 to one less than the tuple size for each tuple.
    • 内部的元素在每个元组中选择元素,因此它们从0增加到小于每个元组的元组大小。
  • 我们构建两个扁平的索引序列,它们的大小都等于所有元组大小的总和:外部索引选择元组,因此它们重复相同的值(tuple包中的元组索引),其数量等于每个元组的大小。内部的元素在每个元组中选择元素,因此它们从0增加到小于每个元组的元组大小。

Once we have that in place, we just expand both index sequences in the call to f.

一旦我们有了这个,我们只需要在调用f中扩展两个索引序列。

#include <tuple>
#include <array>
#include <cstddef>
#include <utility>
#include <type_traits>
#include <iostream>

template<std::size_t S, class... Ts> constexpr auto make_indices()
{
   constexpr std::size_t sizes[] = {std::tuple_size_v<std::remove_reference_t<Ts>>...};
   using arr_t = std::array<std::size_t, S>;
   std::pair<arr_t, arr_t> ret{};
   for(std::size_t c = 0, i = 0; i < sizeof...(Ts); ++i)
      for(std::size_t j = 0; j < sizes[i]; ++j, ++c)
      {
         ret.first[c] = i;
         ret.second[c] = j;
      }
   return ret;
}

template<class F, class... Tuples, std::size_t... OuterIs, std::size_t... InnerIs> 
constexpr decltype(auto) multi_apply_imp_2(std::index_sequence<OuterIs...>, std::index_sequence<InnerIs...>, 
                                           F&& f, std::tuple<Tuples...>&& t)
{
   return std::forward<F>(f)(std::get<InnerIs>(std::get<OuterIs>(std::move(t)))...);
}

template<class F, class... Tuples, std::size_t... Is> 
constexpr decltype(auto) multi_apply_imp_1(std::index_sequence<Is...>, 
                                           F&& f, std::tuple<Tuples...>&& t)
{
   constexpr auto indices = make_indices<sizeof...(Is), Tuples...>();
   return multi_apply_imp_2(std::index_sequence<indices.first[Is]...>{}, std::index_sequence<indices.second[Is]...>{},
      std::forward<F>(f), std::move(t));
}

template<class F, class... Tuples> 
constexpr decltype(auto) multi_apply(F&& f, Tuples&&... ts)
{
   constexpr std::size_t flat_s = (0U + ... + std::tuple_size_v<std::remove_reference_t<Tuples>>);
   if constexpr(flat_s != 0)
      return multi_apply_imp_1(std::make_index_sequence<flat_s>{}, 
         std::forward<F>(f), std::forward_as_tuple(std::forward<Tuples>(ts)...));
   else
      return std::forward<F>(f)();
}

int main()
{
   auto t0 = std::make_tuple(1, 2);
   auto t1 = std::make_tuple(3, 6, 4, 5);
   auto sum = [](auto... xs) { return (0 + ... + xs); };

   std::cout << multi_apply(sum, t0, t1, std::make_tuple(7)) << '\n';
}

It compiles on the trunk versions of Clang and GCC in C++1z mode. In terms of generated code, GCC with -O2 optimizes the call to multi_apply to a constant 28.

它在c++ 1z模式中编译Clang和GCC的主干版本。在生成的代码方面,GCC与-O2优化调用multi_apply到一个常量28。


Replacing std::array with a built-in array inside make_indices by using arr_t = std::size_t[S]; makes it compile on Clang 3.9.1 (that version of libc++ lacks constexpr on std::array's operator[]).

使用arr_t = std::size_t[S]替换std::在make_index中内置数组。使它在Clang 3.9.1上编译(该版本的libc++在std上缺少constexpr::array的运算符[])。

Further replacing std::tuple_size_v with std::tuple_size<X>::value and removing the if constexpr test in multi_apply makes it compile on GCC 6.3.0. (The test handles the cases when no tuples are passed in or all tuples passed in are empty.)

进一步替换std::tuple_size_v与std::tuple_size ::值,并在multi_apply中删除if constexpr测试,使其编译在GCC 6.3.0上。(测试处理没有元组传入或所有元组传递的情况为空的情况。)

Further replacing the uses of fold expressions with calls like

进一步替换使用类似调用的折叠表达式。

sum_array({std::tuple_size_v<std::remove_reference_t<Tuples>>...})

where sum_array can be something simple like

sum_array可以是什么简单的东西?

template<class T, std::size_t S> constexpr T sum_array(const T (& a)[S], std::size_t i = 0)
{
   return i < S ? a[i] + sum_array(a, i + 1) : 0;
}

makes it compile on the latest MSVC 2017 RC (MSVC actually has std::tuple_size_v, but it needs the other changes). The generated code is still great: after replacing the body of the sum lambda with sum_array({xs...}), the resulting code is a direct call to sum_array with the array built in-place directly from the elements of all tuples, so the multi_apply machinery doesn't introduce any run time overhead.

让它在最新的MSVC 2017 RC中编译(MSVC实际上有std::tuple_size_v,但是它需要其他的更改)。生成的代码仍然很好:在用sum_array({xs.})替换sum的主体之后,生成的代码直接调用sum_array,该数组直接从所有元组的元素中构建,因此multi_apply机器不会引入任何运行时开销。


std::apply is defined in terms of INVOKE, so, to keep things consistent, the final call to f should be

std::apply是在调用方面定义的,因此,为了保持一致,对f的最后调用应该是。

std::invoke(std::forward<F>(f), std::get<InnerIs>(std::get<OuterIs>(std::move(t)))...)

Implementations may provide a noexcept-specifier on std::apply (at least, libc++ does; libstdc++ and MSVC currently don't) so that may be worth considering too.

实现可以在std上提供一个noexception -specifier::apply(至少,libc++做;libstdc++和MSVC目前没有这样做,所以这也值得考虑。

#1


10  

Here's my take on it. It doesn't use recursion and it expands those tuples in the same pack expansion, but it requires a bit of preparation:

这是我的看法。它不使用递归,它在相同的包扩展中扩展了这些元组,但它需要一点准备:

  • We build a tuple of references to the tuples passed in, rvalue references for rvalue arguments, lvalue references for lvalue arguments, in order to have proper forwarding in the final call (exactly what std::forward_as_tuple does, as T.C. noted in the comments). The tuple is built and passed around as an rvalue, so reference collapsing ensures correct value categories for each argument in the final call to f.
  • 我们为传入的元组构建了一个元组,为rvalue参数提供了rvalue引用,为lvalue参数提供了lvalue引用,以便在最终调用中有适当的转发(std::forward_as_tuple,正如T.C.在评论中提到的那样)。tuple是作为一个rvalue构建和传递的,因此引用崩溃将确保在对f的最后调用中每个参数的正确值类别。
  • We build two flattened index sequences, both of size equal to the sum of all tuple sizes:
    • The outer indices select the tuple, so they repeat the same value (the tuple's index in the tuple pack) a number of times equal to the size of each tuple.
    • 外部索引选择元组,因此它们重复相同的值(tuple包中的元组索引),其数量等于每个元组的大小。
    • The inner ones select the element in each tuple, so they increase from 0 to one less than the tuple size for each tuple.
    • 内部的元素在每个元组中选择元素,因此它们从0增加到小于每个元组的元组大小。
  • 我们构建两个扁平的索引序列,它们的大小都等于所有元组大小的总和:外部索引选择元组,因此它们重复相同的值(tuple包中的元组索引),其数量等于每个元组的大小。内部的元素在每个元组中选择元素,因此它们从0增加到小于每个元组的元组大小。

Once we have that in place, we just expand both index sequences in the call to f.

一旦我们有了这个,我们只需要在调用f中扩展两个索引序列。

#include <tuple>
#include <array>
#include <cstddef>
#include <utility>
#include <type_traits>
#include <iostream>

template<std::size_t S, class... Ts> constexpr auto make_indices()
{
   constexpr std::size_t sizes[] = {std::tuple_size_v<std::remove_reference_t<Ts>>...};
   using arr_t = std::array<std::size_t, S>;
   std::pair<arr_t, arr_t> ret{};
   for(std::size_t c = 0, i = 0; i < sizeof...(Ts); ++i)
      for(std::size_t j = 0; j < sizes[i]; ++j, ++c)
      {
         ret.first[c] = i;
         ret.second[c] = j;
      }
   return ret;
}

template<class F, class... Tuples, std::size_t... OuterIs, std::size_t... InnerIs> 
constexpr decltype(auto) multi_apply_imp_2(std::index_sequence<OuterIs...>, std::index_sequence<InnerIs...>, 
                                           F&& f, std::tuple<Tuples...>&& t)
{
   return std::forward<F>(f)(std::get<InnerIs>(std::get<OuterIs>(std::move(t)))...);
}

template<class F, class... Tuples, std::size_t... Is> 
constexpr decltype(auto) multi_apply_imp_1(std::index_sequence<Is...>, 
                                           F&& f, std::tuple<Tuples...>&& t)
{
   constexpr auto indices = make_indices<sizeof...(Is), Tuples...>();
   return multi_apply_imp_2(std::index_sequence<indices.first[Is]...>{}, std::index_sequence<indices.second[Is]...>{},
      std::forward<F>(f), std::move(t));
}

template<class F, class... Tuples> 
constexpr decltype(auto) multi_apply(F&& f, Tuples&&... ts)
{
   constexpr std::size_t flat_s = (0U + ... + std::tuple_size_v<std::remove_reference_t<Tuples>>);
   if constexpr(flat_s != 0)
      return multi_apply_imp_1(std::make_index_sequence<flat_s>{}, 
         std::forward<F>(f), std::forward_as_tuple(std::forward<Tuples>(ts)...));
   else
      return std::forward<F>(f)();
}

int main()
{
   auto t0 = std::make_tuple(1, 2);
   auto t1 = std::make_tuple(3, 6, 4, 5);
   auto sum = [](auto... xs) { return (0 + ... + xs); };

   std::cout << multi_apply(sum, t0, t1, std::make_tuple(7)) << '\n';
}

It compiles on the trunk versions of Clang and GCC in C++1z mode. In terms of generated code, GCC with -O2 optimizes the call to multi_apply to a constant 28.

它在c++ 1z模式中编译Clang和GCC的主干版本。在生成的代码方面,GCC与-O2优化调用multi_apply到一个常量28。


Replacing std::array with a built-in array inside make_indices by using arr_t = std::size_t[S]; makes it compile on Clang 3.9.1 (that version of libc++ lacks constexpr on std::array's operator[]).

使用arr_t = std::size_t[S]替换std::在make_index中内置数组。使它在Clang 3.9.1上编译(该版本的libc++在std上缺少constexpr::array的运算符[])。

Further replacing std::tuple_size_v with std::tuple_size<X>::value and removing the if constexpr test in multi_apply makes it compile on GCC 6.3.0. (The test handles the cases when no tuples are passed in or all tuples passed in are empty.)

进一步替换std::tuple_size_v与std::tuple_size ::值,并在multi_apply中删除if constexpr测试,使其编译在GCC 6.3.0上。(测试处理没有元组传入或所有元组传递的情况为空的情况。)

Further replacing the uses of fold expressions with calls like

进一步替换使用类似调用的折叠表达式。

sum_array({std::tuple_size_v<std::remove_reference_t<Tuples>>...})

where sum_array can be something simple like

sum_array可以是什么简单的东西?

template<class T, std::size_t S> constexpr T sum_array(const T (& a)[S], std::size_t i = 0)
{
   return i < S ? a[i] + sum_array(a, i + 1) : 0;
}

makes it compile on the latest MSVC 2017 RC (MSVC actually has std::tuple_size_v, but it needs the other changes). The generated code is still great: after replacing the body of the sum lambda with sum_array({xs...}), the resulting code is a direct call to sum_array with the array built in-place directly from the elements of all tuples, so the multi_apply machinery doesn't introduce any run time overhead.

让它在最新的MSVC 2017 RC中编译(MSVC实际上有std::tuple_size_v,但是它需要其他的更改)。生成的代码仍然很好:在用sum_array({xs.})替换sum的主体之后,生成的代码直接调用sum_array,该数组直接从所有元组的元素中构建,因此multi_apply机器不会引入任何运行时开销。


std::apply is defined in terms of INVOKE, so, to keep things consistent, the final call to f should be

std::apply是在调用方面定义的,因此,为了保持一致,对f的最后调用应该是。

std::invoke(std::forward<F>(f), std::get<InnerIs>(std::get<OuterIs>(std::move(t)))...)

Implementations may provide a noexcept-specifier on std::apply (at least, libc++ does; libstdc++ and MSVC currently don't) so that may be worth considering too.

实现可以在std上提供一个noexception -specifier::apply(至少,libc++做;libstdc++和MSVC目前没有这样做,所以这也值得考虑。