繁体   English   中英

将多个元组应用于同一函数(例如,apply(f,tuples ...)`),而无需递归或`tuple_cat`

[英]Applying multiple tuples to the same function (i.e. `apply(f, tuples…)`) without recursion or `tuple_cat`

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

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

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


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

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

用法示例:

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

我可以想到实现multi_apply的各种幼稚方式:

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

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

但是我要问的是: 如何在不借助std::tuple_cat或递归的情况下实现multi_apply

我理想地想做的是:为每个元组生成一个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)...);
} 

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

  • 我们建立对传入的元组的引用的元组,rvalue参数的rvalue引用,lvalue参数的lvalue引用,以便在最终调用中正确转发(正如TC在注释中指出的那样,正是std::forward_as_tuple所做的事情) 。 元组是作为右值构建和传递的,因此引用折叠可确保最终调用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';
}

它在C ++ 1z模式下在Clang和GCC的主干版本上编译。 就生成的代码而言,带有-O2 GCC multi_apply的调用优化为常数28


通过using arr_t = std::size_t[S];make_indices中用内置std::array替换std::array using arr_t = std::size_t[S]; 使其可以在Clang 3.9.1上编译(该libc ++版本在std::arrayoperator[]上缺少constexpr )。

进一步用std::tuple_size_v std::tuple_size<X>::value替换std::tuple_size_v并删除multi_applyif constexpr测试,使其可以在GCC 6.3.0上编译。 (该测试处理没有传入任何元组或传入的所有元组为空的情况。)

进一步将fold表达式的用法替换为诸如

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

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

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


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

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

实现可能会在std::apply上提供noexcept-specifier(至少,libc ++可以; libstdc ++和MSVC当前不提供),因此也可能值得考虑。

替代版本:

template <class F, std::size_t... Is, class ... Ts>
constexpr decltype(auto) multiple_apply_impl(F&& f, std::index_sequence<Is...>, Ts&&... ts)
{
    constexpr auto p = [](){
        constexpr auto total_size = sizeof...(Is);
        std::array<std::size_t, total_size> outer{};
        std::array<std::size_t, total_size> inner{};
        std::size_t global_index = 0;
        std::size_t outer_value = 0;

        [[maybe_unused]] auto l = [&](std::size_t size)
        {
            for (std::size_t i = 0; i != size; ++i) {
                outer[global_index] = outer_value;
                inner[global_index] = i;
                ++global_index;
            }
            ++outer_value;
        };
        (l(std::tuple_size<std::decay_t<Ts>>::value), ...);

        return make_pair(outer, inner);
    }();
    [[maybe_unused]] constexpr auto outer = p.first;
    [[maybe_unused]] constexpr auto inner = p.second;

    using std::get;
    return std::invoke(std::forward<F>(f),
                       get<inner[Is]>(get<outer[Is]>(std::forward_as_tuple(std::forward<Ts>(ts)...)))...);
}

template <class F, class ... Ts>
constexpr decltype(auto) multiple_apply(F&& f, Ts&&... ts)
{
    constexpr auto total_size = (std::size_t{0} + ... + std::tuple_size<std::decay_t<Ts>>::value);

    return multiple_apply_impl(std::forward<F>(f),
                               std::make_index_sequence<total_size>(),
                               std::forward<Ts>(ts)...);
}

演示版

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM