在结构上应用标准算法?
applying std algorithms over structs?
我们有Boost.PFR and we have the tuple iterator。如果我们将两者结合起来,我们可能有一种在结构上应用 std 算法的方法。解决方案是否已经存在?我正在寻找的是:
S a, b;
auto const ra(to_range(a)), rb(to_range(b));
std::transform(ra.begin(), ra.end(), rb.begin(), [](auto&& a)noexcept{return a;});
这将允许我们使用较新的 <execution>
功能来乱序或并行处理结构。
您已经可以使用 pfr::structure_tie
的现有功能,这会产生一个引用元组。
This would allow us to use the newer features to process structs out of sequence or in parallel.
只有当每个元素的处理具有相当大的执行成本时,这才有意义。在那种情况下,您可能已经可以接受定制
boost::pfr::for_each_field(var, [](auto& field) { field += 1; });
调用
为了说明我在 中试图提出的观点,让我们写一些类似你的转换的东西:
直接实施
我跳过了迭代器和标准库的概念,因为它被整个“迭代器值类型必须固定”和其他负担所拖累。
相反,让我们“从功能上”来做。
#include <boost/pfr.hpp>
namespace pfr = boost::pfr;
template <typename Op, typename... T>
void transform(Op f, T&&... operands) {
auto apply = [&]<int N>() {
f(pfr::get<N>(std::forward<T>(operands))...);
return 1;
};
constexpr auto size = std::min({pfr::tuple_size<std::decay_t<T>>::value...});
// optionally assert that sizes match:
//static_assert(size == std::max({pfr::tuple_size<std::decay_t<T>>::value...}));
[=]<auto... N>(std::index_sequence<N...>) {
return (apply.template operator()<N>() + ...);
}
(std::make_index_sequence<size>{});
}
我已经通过不固定元数来概括了一点。它现在更像是一个 n 元 zip 或 visitor。要获得您想要的转换,您需要将其传递给
这样的操作
auto binary = [](auto const& a, auto& b) {
b = a;
};
让我们演示一下,突出显示混合类型成员、非对称类型以及混合长度结构:
struct S1 { int a; double b; long c; float d; };
struct S2 { double a; double b; double c; double d; };
struct S3 { double a; double b; };
测试用例:
int main() {
auto n_ary = [](auto&... fields) {
puts(__PRETTY_FUNCTION__);
return (... = fields);
};
S1 a;
S2 b;
S3 c;
// all directions
transform(binary, a, b);
transform(binary, b, a);
// mixed sizes
transform(binary, b, c);
transform(binary, c, a);
// why settle for binary?
transform(n_ary, a, b);
transform(n_ary, a, b, c);
transform(n_ary, c, b, a);
}
反汇编表明 所有内容 都在进行内联和优化。字面上地。只剩下 puts
个调用:
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
mov edi, OFFSET FLAT:.LC1
call puts
mov edi, OFFSET FLAT:.LC2
call puts
...
...
xor eax, eax
add rsp, 8
ret
给出输出
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = int; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = long int; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = float; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = long int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = float]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(auto:14& ...)> [with auto:14 = {int, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {long int, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {float, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {int, double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, int}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, double}]
工作证明
让我们做一些我们可以检查的“有用”计算。此外,将 transform
函数重命名为 nway_visit
只是反映其更通用的方向:
auto binary = [](auto& a, auto& b) { return a *= b; };
auto n_ary = [](auto&... fields) { return (... *= fields); };
所以这两个操作都进行了右折叠乘法赋值。给定一些明确选择的初始值设定项
S1 a {1,2,3,4};
S2 b {2,3,4,5};
S3 c {3,4};
我们希望能够看到通过数据结构的数据流。因此,让我们有选择地对其进行一些调试跟踪:
#define DEMO(expr) \
void(expr); \
if constexpr (output_enabled) { \
std::cout << "After " << std::left << std::setw(26) << #expr; \
std::cout << " a:" << pfr::io(a) << "\tb:" << pfr::io(b) \
<< "\tc:" << pfr::io(c) << "\n"; \
}
DEMO("initialization");
// all directions
DEMO(nway_visit(binary, a, b));
DEMO(nway_visit(binary, b, a));
// mixed sizes
DEMO(nway_visit(binary, b, c));
DEMO(nway_visit(binary, c, a));
// why settle for binary?
DEMO(nway_visit(n_ary, a, b));
DEMO(nway_visit(n_ary, a, b, c));
DEMO(nway_visit(n_ary, c, b, a));
return long(c.a + c.b) % 37; // prevent whole program optimization...
作为锦上添花,让我们绝对确定(禁用输出)编译器无法优化整个程序,因为没有可观察到的效果:
return long(c.a + c.b) % 37; // prevent whole program optimization...
演示是 Live On Compiler Explorer 启用输出,一次禁用输出显示反汇编:
main:
mov eax, 13
ret
WOW
我的天啊。那就是优化。整个程序是静态评估的,只有 return 退出代码 13。让我们看看这是否是正确的退出代码:
启用输出:
After "initialization" a:{1, 2, 3, 4} b:{2, 3, 4, 5} c:{3, 4}
After nway_visit(binary, a, b) a:{2, 6, 12, 20} b:{2, 3, 4, 5} c:{3, 4}
After nway_visit(binary, b, a) a:{2, 6, 12, 20} b:{4, 18, 48, 100} c:{3, 4}
After nway_visit(binary, b, c) a:{2, 6, 12, 20} b:{12, 72, 48, 100} c:{3, 4}
After nway_visit(binary, c, a) a:{2, 6, 12, 20} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, a, b) a:{24, 432, 576, 2000} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, a, b, c) a:{1728, 746496, 576, 2000} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, c, b, a) a:{1728, 746496, 576, 2000} b:{12, 72, 48, 100} c:{124416, 1289945088}
所以,return 的值应该是 (124416 + 1289945088) modulo 37
,袖珍计算器确认是 13。
从这里开始:并行任务等
您最初的动机包括免费从标准库中获取并行执行选项。如您所知 它的用处。
但是,几乎没有什么能阻止您从算法中获得这种行为:
boost::asio::thread_pool ctx; // or, e.g. system_executor
auto run_task = [&](auto&... fields) {
boost::asio::post(ctx, [=] { long_running_task(fields...); });
};
希望这是很好的灵感。感谢您让我了解 PFR。还蛮甜的。
我们有Boost.PFR and we have the tuple iterator。如果我们将两者结合起来,我们可能有一种在结构上应用 std 算法的方法。解决方案是否已经存在?我正在寻找的是:
S a, b;
auto const ra(to_range(a)), rb(to_range(b));
std::transform(ra.begin(), ra.end(), rb.begin(), [](auto&& a)noexcept{return a;});
这将允许我们使用较新的 <execution>
功能来乱序或并行处理结构。
您已经可以使用 pfr::structure_tie
的现有功能,这会产生一个引用元组。
This would allow us to use the newer features to process structs out of sequence or in parallel.
只有当每个元素的处理具有相当大的执行成本时,这才有意义。在那种情况下,您可能已经可以接受定制
boost::pfr::for_each_field(var, [](auto& field) { field += 1; });
调用
为了说明我在
直接实施
我跳过了迭代器和标准库的概念,因为它被整个“迭代器值类型必须固定”和其他负担所拖累。
相反,让我们“从功能上”来做。
#include <boost/pfr.hpp>
namespace pfr = boost::pfr;
template <typename Op, typename... T>
void transform(Op f, T&&... operands) {
auto apply = [&]<int N>() {
f(pfr::get<N>(std::forward<T>(operands))...);
return 1;
};
constexpr auto size = std::min({pfr::tuple_size<std::decay_t<T>>::value...});
// optionally assert that sizes match:
//static_assert(size == std::max({pfr::tuple_size<std::decay_t<T>>::value...}));
[=]<auto... N>(std::index_sequence<N...>) {
return (apply.template operator()<N>() + ...);
}
(std::make_index_sequence<size>{});
}
我已经通过不固定元数来概括了一点。它现在更像是一个 n 元 zip 或 visitor。要获得您想要的转换,您需要将其传递给
这样的操作auto binary = [](auto const& a, auto& b) {
b = a;
};
让我们演示一下,突出显示混合类型成员、非对称类型以及混合长度结构:
struct S1 { int a; double b; long c; float d; };
struct S2 { double a; double b; double c; double d; };
struct S3 { double a; double b; };
测试用例:
int main() {
auto n_ary = [](auto&... fields) {
puts(__PRETTY_FUNCTION__);
return (... = fields);
};
S1 a;
S2 b;
S3 c;
// all directions
transform(binary, a, b);
transform(binary, b, a);
// mixed sizes
transform(binary, b, c);
transform(binary, c, a);
// why settle for binary?
transform(n_ary, a, b);
transform(n_ary, a, b, c);
transform(n_ary, c, b, a);
}
反汇编表明 所有内容 都在进行内联和优化。字面上地。只剩下 puts
个调用:
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
mov edi, OFFSET FLAT:.LC1
call puts
mov edi, OFFSET FLAT:.LC2
call puts
...
...
xor eax, eax
add rsp, 8
ret
给出输出
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = int; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = long int; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = float; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = long int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = float]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = int]
main()::<lambda(const auto:12&, auto:13&)> [with auto:12 = double; auto:13 = double]
main()::<lambda(auto:14& ...)> [with auto:14 = {int, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {long int, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {float, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {int, double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, double}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, int}]
main()::<lambda(auto:14& ...)> [with auto:14 = {double, double, double}]
工作证明
让我们做一些我们可以检查的“有用”计算。此外,将 transform
函数重命名为 nway_visit
只是反映其更通用的方向:
auto binary = [](auto& a, auto& b) { return a *= b; };
auto n_ary = [](auto&... fields) { return (... *= fields); };
所以这两个操作都进行了右折叠乘法赋值。给定一些明确选择的初始值设定项
S1 a {1,2,3,4};
S2 b {2,3,4,5};
S3 c {3,4};
我们希望能够看到通过数据结构的数据流。因此,让我们有选择地对其进行一些调试跟踪:
#define DEMO(expr) \
void(expr); \
if constexpr (output_enabled) { \
std::cout << "After " << std::left << std::setw(26) << #expr; \
std::cout << " a:" << pfr::io(a) << "\tb:" << pfr::io(b) \
<< "\tc:" << pfr::io(c) << "\n"; \
}
DEMO("initialization");
// all directions
DEMO(nway_visit(binary, a, b));
DEMO(nway_visit(binary, b, a));
// mixed sizes
DEMO(nway_visit(binary, b, c));
DEMO(nway_visit(binary, c, a));
// why settle for binary?
DEMO(nway_visit(n_ary, a, b));
DEMO(nway_visit(n_ary, a, b, c));
DEMO(nway_visit(n_ary, c, b, a));
return long(c.a + c.b) % 37; // prevent whole program optimization...
作为锦上添花,让我们绝对确定(禁用输出)编译器无法优化整个程序,因为没有可观察到的效果:
return long(c.a + c.b) % 37; // prevent whole program optimization...
演示是 Live On Compiler Explorer 启用输出,一次禁用输出显示反汇编:
main:
mov eax, 13
ret
WOW
我的天啊。那就是优化。整个程序是静态评估的,只有 return 退出代码 13。让我们看看这是否是正确的退出代码:
启用输出:
After "initialization" a:{1, 2, 3, 4} b:{2, 3, 4, 5} c:{3, 4}
After nway_visit(binary, a, b) a:{2, 6, 12, 20} b:{2, 3, 4, 5} c:{3, 4}
After nway_visit(binary, b, a) a:{2, 6, 12, 20} b:{4, 18, 48, 100} c:{3, 4}
After nway_visit(binary, b, c) a:{2, 6, 12, 20} b:{12, 72, 48, 100} c:{3, 4}
After nway_visit(binary, c, a) a:{2, 6, 12, 20} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, a, b) a:{24, 432, 576, 2000} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, a, b, c) a:{1728, 746496, 576, 2000} b:{12, 72, 48, 100} c:{6, 24}
After nway_visit(n_ary, c, b, a) a:{1728, 746496, 576, 2000} b:{12, 72, 48, 100} c:{124416, 1289945088}
所以,return 的值应该是 (124416 + 1289945088) modulo 37
,袖珍计算器确认是 13。
从这里开始:并行任务等
您最初的动机包括免费从标准库中获取并行执行选项。如您所知
但是,几乎没有什么能阻止您从算法中获得这种行为:
boost::asio::thread_pool ctx; // or, e.g. system_executor
auto run_task = [&](auto&... fields) {
boost::asio::post(ctx, [=] { long_running_task(fields...); });
};
希望这是很好的灵感。感谢您让我了解 PFR。还蛮甜的。