c++ 使用范围过滤并排向量同时保持可读

lxkprmvk  于 2023-03-20  发布在  其他
关注(0)|答案(4)|浏览(106)

假设我有两个向量(原因与我正在使用的框架有关):

std::vector<bool> valid = {false, true, true, false};
std::vector<int> percentages = {2, 50, 3, 100};

这些向量代表电池的有效性和充电百分比。我想找到有效电池的最小充电量。要用范围来做这件事,我能想到的最好的是:

auto min = std::get<1>(
    std::ranges::min(
        std::views::zip(valid, percentages) | 
        std::views::filter([](const auto& vals) {return std::get<0>(vals);}), 
    [](const auto& vals0, const auto& vals1) {
    return std::get<1>(vals0) < std::get<1>(vals1);
}));

现在是this works,但是几乎不可能读取,所以for循环的变体应该是:

int min_percentage = 100;
for (const auto& [is_valid, percentage] : std::views::zip(valid, percentages)) {
    if (is_valid) {
        min_percentage = std::min(min_percentage, percentage);
    }
}

哪个works just fine as well
第二种方法客观上更好,所以问题是,有没有一种方法可以在保持代码可读性的同时用范围来编写它?

hts6caw3

hts6caw31#

一般来说,这是可读性差的原因:

auto min = std::get<1>(
    std::ranges::min(
        std::views::zip(valid, percentages) | 
        std::views::filter([](const auto& vals) {return std::get<0>(vals);}), 
    [](const auto& vals0, const auto& vals1) {
    return std::get<1>(vals0) < std::get<1>(vals1);
}));

这是一个错误的算法--你想找到最小的 percentage,但是你现在的算法是按百分比找到最小的 * 对(valid,percentage)*。
简单地重构到正确的算法(最小百分比)是一个很大的改进:

auto min = 
    std::ranges::min(
        std::views::zip(valid, percentages)
        | std::views::filter([](const auto& vals) {return std::get<0>(vals);})
        | std::views::values);

现在有一个提议(P2769),让你把lambda写成std::ranges::get_key(或者std::ranges::get_element<0>)。
还要注意,传入min的 predicate 并不是绝对必要的--默认的<已经做了正确的事情,因为该范围内的所有元组现在都是(true, x),所以根据定义,那里的最小值是x最小的值。
这是其中一种情况,就像我们缺少了一个算法:

ranges::min(
    views::zip(valid, percentages)
    | views::filter_map([](auto&& e) -> optional<int> {
        auto& [valid, perc] = e;
        if (valid) {
            return perc;
        } else {
            return nullopt;
        }
    })
);

其中,filter_mapT -> optional<U>,并生成范围U
不过,在这种情况下,你可以作弊--你用for循环编写的算法可以用transform来完成:你把无效电池Map到100:

ranges::min(
    views::zip(valid, percentages)
    | views::transform([](auto&& e) {
        auto& [valid, perc] = e;
        return valid ? perc : 100;
    })
);

或者更直接地说:

ranges::min(
    views::zip_transform([](bool valid, int perc){
        return valid ? perc : 100;
    }, valid, percentages)
);

这在没有有效电池的情况下有不同的语义(你得到一个定义良好的100而不是UB)。尽管你的循环即使在没有电池的情况下也是定义良好的(你得到100),而在这里空的情况下仍然是UB。

omjgkv6w

omjgkv6w2#

我觉得如果你给这些羊起个名字的话是很容易理解的:

auto is_valid = [](auto const& zipped) { return std::get<0>(zipped); };

auto min = std::ranges::min(
    std::views::zip(valid, percentages) |
    std::views::filter(is_valid) |
    std::views::values
);

https://godbolt.org/z/95T8xsq68

pgpifvop

pgpifvop3#

关于:

namespace rv=std::views;
auto min = std::ranges::min(
    rv::zip(valid, percentages) | 
    rv::filter([](const auto& x)
        {return std::get<0>(x);}) |
    rv::values);

同样,您可以使用rv::elements<1>代替rv::values

xzlaal3s

xzlaal3s4#

理想情况下,我们可以只传递std::get<0>作为过滤函数。
不幸的是,编译器无法推导出重载,所以我们必须用lambda来帮助它。由于这是一个常见的问题(How do I specify a pointer to an overloaded function?),在您的项目中为它提供一个帮助宏可能是合理的:

#define AS_LAMBDA(...) [&](auto&&... args)                          \
    -> decltype(__VA_ARGS__(std::forward<decltype(args)>(args)...)) \
{                                                                   \
    return __VA_ARGS__(std::forward<decltype(args)>(args)...);      \
}

这使得代码看起来如下所示:

auto min = std::ranges::min(
   std::views::zip(valid, percentages) |
   std::views::filter(AS_LAMBDA(std::get<0>)) |
   std::views::elements<1>
);

https://godbolt.org/z/Gaa1ErdGG

相关问题