在结构上应用标准算法?

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

看到了Live On Compiler Explorer

反汇编表明 所有内容 都在进行内联和优化。字面上地。只剩下 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。还蛮甜的。