将多个元组应用于同一个函数(即`apply(f,tuples ...)`)而不进行递归或`tuple_cat`

Vit*_*meo 10 c++ templates tuples variadic-templates c++17

std::experimental::apply 有以下签名:

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t);
Run Code Online (Sandbox Code Playgroud)

它基本上f通过扩展t元素作为参数来调用.


我想要的东西完全相同,但同时有多个元组:

template <class F, class... Tuples>
constexpr decltype(auto) multi_apply(F&& f, Tuples&&... ts);
Run Code Online (Sandbox Code Playgroud)

用法示例:

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);
Run Code Online (Sandbox Code Playgroud)

我可以想到各种天真的实施方式multi_apply:

  • 使用std::tuple_cat然后调用std::experimental::apply.

  • 使用递归将每个元组的参数绑定到一系列最终调用原始函数的lambda.

但我要问的是:如何在multi_apply不诉诸std::tuple_cat或递归的情况下实施?

理想情况下,我想做的是:std::index_sequence为每个元组生成一个元组,并在同一个可变扩展中将每个元组与自己的索引序列进行匹配.这可能吗?例:

// 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)...);
} 
Run Code Online (Sandbox Code Playgroud)

bog*_*dan 11

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

  • 我们构建了一个对传入的元组的引用元组,rvalue参数的rvalue引用,lvalue参数的左值引用,以便在最终调用中进行适当的转发(std::forward_as_tuple正如TC中注释的那样).元组是作为右值构建和传递的,因此引用折叠可确保最终调用中每个参数的正确值类别f.
  • 我们构建了两个扁平索引序列,两者的大小都等于所有元组大小的总和:
    • 外部索引选择元组,因此它们重复相同的值(元组包中的元组索引)的次数等于每个元组的大小.
    • 内部元素选择每个元组中的元素,因此它们从0每个元组的元组大小增加到一个.

一旦我们有了这个,我们只需在调用中扩展两个索引序列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';
}
Run Code Online (Sandbox Code Playgroud)

它在C++ 1z模式下编译Clang和GCC的主干版本.就生成的代码而言,GCC -O2优化了multi_apply对常量的调用28.


更换std::array内部具有内置的阵列make_indices通过using arr_t = std::size_t[S];使得汇编锵3.9.1(的libc ++该版本缺乏constexprstd::arrayoperator[]).

进一步替换std::tuple_size_vstd::tuple_size<X>::value和除去if constexpr试验中multi_apply使得它汇编GCC 6.3.0.(测试处理没有传入元组或传入的所有元组都为空的情况.)

进一步用如调用替换fold表达式的用法

sum_array({std::tuple_size_v<std::remove_reference_t<Tuples>>...})
Run Code Online (Sandbox Code Playgroud)

哪里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;
}
Run Code Online (Sandbox Code Playgroud)

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


std::apply根据INVOKE定义,因此,为了保持一致,最终的调用f应该是

std::invoke(std::forward<F>(f), std::get<InnerIs>(std::get<OuterIs>(std::move(t)))...)
Run Code Online (Sandbox Code Playgroud)

实现可能会提供一个noexcept-specifier std::apply(至少libc ++会这样; libstdc ++和MSVC目前没有),因此也值得考虑.

  • @TC绝对正确.只要看看我的spankin'新发明的轮子......它就像另一个轮子,但它是*我的*. (2认同)