将多个元组应用于同一个函数(即 `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)...);
}
这是我的看法。它不使用递归并在同一个包扩展中扩展那些元组,但它需要一些准备工作:
- 我们构建了一个元组引用元组,右值参数的右值引用,左值参数的左值引用,以便在最终调用中进行正确的转发(正是
std::forward_as_tuple
所做的,如T.C。在评论中注明)。元组被构建并作为右值传递,因此引用折叠确保在最终调用 f
. 时每个参数的值类别正确
- 我们构建了两个扁平索引序列,它们的大小都等于所有元组大小的总和:
- 外部索引 select 元组,因此它们重复相同的值(元组在元组包中的索引)的次数等于每个元组的大小。
- 内部的 select 每个元组中的元素,因此它们从
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 模式在 t运行k 版本的 Clang 和 GCC 上编译。就生成的代码而言,具有 -O2
的 GCC 将对 multi_apply
的调用优化为常量 28
.
用 make_indices
中的 built-in 数组替换 std::array
通过 using arr_t = std::size_t[S];
使其在 Clang 3.9.1 上编译(该版本的 libc++ 缺少 constexpr
在 std::array
的 operator[]
上)。
进一步用 std::tuple_size<X>::value
替换 std::tuple_size_v
并删除 multi_apply
中的 if constexpr
测试使其在 GCC 6.3.0 上编译。 (测试处理没有传入元组或传入的所有元组为空的情况。)
进一步用
之类的调用替换折叠表达式的使用
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
lambda 的主体替换为 sum_array({xs...})
后,生成的代码直接调用 sum_array
并直接从 in-place 构建数组所有元组的元素,因此 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)...);
}
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)...);
}
这是我的看法。它不使用递归并在同一个包扩展中扩展那些元组,但它需要一些准备工作:
- 我们构建了一个元组引用元组,右值参数的右值引用,左值参数的左值引用,以便在最终调用中进行正确的转发(正是
std::forward_as_tuple
所做的,如T.C。在评论中注明)。元组被构建并作为右值传递,因此引用折叠确保在最终调用f
. 时每个参数的值类别正确
- 我们构建了两个扁平索引序列,它们的大小都等于所有元组大小的总和:
- 外部索引 select 元组,因此它们重复相同的值(元组在元组包中的索引)的次数等于每个元组的大小。
- 内部的 select 每个元组中的元素,因此它们从
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 模式在 t运行k 版本的 Clang 和 GCC 上编译。就生成的代码而言,具有 -O2
的 GCC 将对 multi_apply
的调用优化为常量 28
.
用 make_indices
中的 built-in 数组替换 std::array
通过 using arr_t = std::size_t[S];
使其在 Clang 3.9.1 上编译(该版本的 libc++ 缺少 constexpr
在 std::array
的 operator[]
上)。
进一步用 std::tuple_size<X>::value
替换 std::tuple_size_v
并删除 multi_apply
中的 if constexpr
测试使其在 GCC 6.3.0 上编译。 (测试处理没有传入元组或传入的所有元组为空的情况。)
进一步用
之类的调用替换折叠表达式的使用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
lambda 的主体替换为 sum_array({xs...})
后,生成的代码直接调用 sum_array
并直接从 in-place 构建数组所有元组的元素,因此 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)...);
}