简体   繁体   English

如何使用具有两个范围的 `transform()` 的范围版本?

[英]How to use the range version of `transform()` with two ranges?

The header <algorithm> contains a version of std::transform() taking a two input sequences, an output sequence, and a binary function as parameters, eg: header <algorithm>包含一个std::transform()版本,它采用两个输入序列、一个 output 序列和一个二进制 ZC1C425268E68385D1AB5074C17A94F1Z 作为参数,例如:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>

int main()
{
    std::vector<int> v0{1, 2, 3};
    std::vector<int> v1{4, 5, 6};
    std::vector<int> result;
    std::transform(v0.begin(), v0.end(), v1.begin(), std::back_inserter(result),
                   [](auto a, auto b){ return a + b; });
    std::copy(result.begin(), result.end(),
              std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\n';
}

C++20 introduced range algoirthms which does include std::ranges::views::transform(R, F) and its implementation std::ranges::views::transform_view . C++20 引入了范围算法,其中包括std::ranges::views::transform(R, F)及其实现std::ranges::views::transform_view I can see how to use this transform() with one range, eg:我可以看到如何在一个范围内使用这个transform() ,例如:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <ranges>
#include <vector>

int main()
{   
    std::vector<int> v0{1, 2, 3}; 
    for (auto x: std::ranges::views::transform(v0, [](auto a){ return a + 3; })) {
        std::cout << x << ' ';
    }   
    std::cout << '\n';
}   

However, there is no version supporting more than one range.但是,没有版本支持多个范围。 So, the question becomes: How to use the range version of transform() with two (or more) ranges?所以,问题就变成了:如何将范围版本的transform()与两个(或更多)范围一起使用? On objective of this approach is to benefit from the lazy evaluation of views and avoid the creation of an intermediate sequence ( result in the non-ranges version above).这种方法的目标是从视图的惰性评估中受益并避免创建中间序列( result上面的非范围版本)。 A potential use could look like this (putting the function argument in front to make it easier for a potential solution allowing even more than two ranges):潜在用途可能如下所示(将 function 参数放在前面,以便更容易实现允许两个以上范围的潜在解决方案):

for (auto v: envisioned::transform([](auto a, auto b){ return a + b; }, v0, v1) {
    std::cout << v << ' ';
}
std::cout << '\n';

The way you would like to use transform , where you take an arbitrary number of input ranges is not possible directly with what's available in <algorithm> as of C++20.从 C++20 开始, <algorithm>中可用的内容无法直接使用您想要使用transform的方式,您可以在其中获取任意数量的输入范围。 You can of course write such an algorithm yourself without too much effort.您当然可以自己编写这样的算法而无需太多努力。

The example with 2 input ranges can be implemented in C++20 like this:具有 2 个输入范围的示例可以在 C++20 中实现,如下所示:

std::ranges::transform(v0, v1,
                       std::ostream_iterator<int>(std::cout, " "),
                       std::plus{});    

Here's a demo , and this is specifically the last overload of transform listed here .这是一个演示,特别是此处列出的最后一个transform重载。

There is unfortunately no way to write the equivalent version like this:不幸的是,没有办法像这样编写等效版本:

for (auto v : std::views::transform(v0, v1, std::plus{})) // no, unfortunately    
  std::cout << v << " ";

but the above implementation does the same thing.但上面的实现做同样的事情。 It certainly satisfies your requirements of not having to store the results separately;它当然可以满足您不必单独存储结果的要求; then can be printed as they're generated.然后可以在生成时打印。

What you're looking for is the algorithm that range-v3 calls zip_with and what we are proposing in P2214 to add to C++23 under the name zip_transform .您正在寻找的是 range-v3 调用zip_with的算法,以及我们在P2214中提出的以名称zip_transform添加到 C++23 的算法。 There is no such algorithm in C++20. C++20 中没有这样的算法。

Until then, the range-v3 version is exactly your use-case:在此之前,range-v3 版本正是您的用例:

for (auto v : zip_with([](auto a, auto b){ return a + b; }, v0, v1)) {
    std::cout << v << ' ';
}

It can handle an arbitrary number of ranges.它可以处理任意数量的范围。

Note that there is no piping version here, just as there is not with regular zip .请注意,这里没有管道版本,就像没有常规zip

The answer blow is how I envisioned to answer the question and I think it still contains some interesting bits on how to actually implement a view.答案是我设想如何回答这个问题,我认为它仍然包含一些关于如何实际实现视图的有趣部分。 It turns out that P2214 mentioned in @Barry's answer has an interesting view ( zip_transform ) which does an intermediate step of the solution posted below but actually fully covers the functionality needed to do a multi-range transform !事实证明, @Barry 的答案中提到的P2214有一个有趣的视图( zip_transform ),它执行了下面发布的解决方案的中间步骤,但实际上完全涵盖了进行多范围transform所需的功能!

It seems there are essentially two ingredients to using std::ranges::views::transform() with multiple ranges:使用具有多个范围的std::ranges::views::transform()似乎基本上有两个要素:

  1. Some way to zip the objects at the corresponding positions of the ranges into a std::tuple , probably retaining the value category of the respective values.某种方式将zip范围内相应位置的对象放入std::tuple ,可能保留各个值的值类别。
  2. Instead of using an n-ary function to take the elements of the range as parameters the function would rather use a std::tuple and possibly use that to call a corresponding n-ary function.与其使用 n 元 function 将范围的元素作为参数,function 宁愿使用std::tuple并可能使用它来调用相应的 n 元 ZC1C425268E68785D1AB5074C1F1。

Using this idea would allow creating a version of transform() dealing with an arbitrary number of ranges, although it is easier to take the function object first rather than extract the last element of a parameter pack:使用这个想法将允许创建一个处理任意数量范围的transform()版本,尽管首先使用 function object 而不是提取参数包的最后一个元素更容易:

auto transform(auto&& fun, auto&&... ranges)
{
    return std::ranges::views::transform(zip(std::forward<decltype(ranges)>(ranges)...),
                                         [fun = std::forward<decltype(fun)>(fun)]
                                         (auto&& t){ return std::apply(fun, std::forward<decltype(t)>(t)); });
}

The zip view used by this implementation can be implemented in terms of std::tuple :此实现使用的zip视图可以根据std::tuple来实现:

template <typename... Range>
struct zip_view
    : std::ranges::view_base
{
    template <typename V>
    struct rvalue_view
    {
        std::shared_ptr<std::decay_t<V>> view;
        rvalue_view() = default;
        rvalue_view(V v): view(new std::decay_t<V>(std::move(v))) {}
        auto begin() const { return this->view->begin(); }
        auto end() const { return this->view->end(); }
    };
    template <typename T>
    using element_t = std::conditional_t<
        std::is_rvalue_reference_v<T>,
        rvalue_view<T>,
        T
        >;
    using storage_t = std::tuple<element_t<Range>...>;
    using value_type = std::tuple<std::ranges::range_reference_t<std::remove_reference_t<Range>>...>;
    using reference = value_type;
    using difference_type = std::common_type_t<std::ranges::range_difference_t<Range>...>;
    storage_t ranges;
    
    template <typename> struct base;
    template <std::size_t... I>
    struct base<std::integer_sequence<std::size_t, I...>>
    {
        using value_type = zip_view::value_type;
        using reference = zip_view::value_type;
        using pointer = value_type*;
        using difference_type = std::common_type_t<std::ranges::range_difference_t<Range>...>;
        using iterator_category = std::common_type_t<std::random_access_iterator_tag,
                                                     typename std::iterator_traits<std::ranges::iterator_t<Range>>::iterator_category...>;

        using iterators_t = std::tuple<std::ranges::iterator_t<Range>...>;
        iterators_t iters;

        reference operator*() const { return {*std::get<I>(iters)...}; }
        reference operator[](difference_type n) const { return {std::get<I>(iters)[n]...}; }
        void increment() { (++std::get<I>(iters), ...); }
        void decrement() { (--std::get<I>(iters), ...); }
        bool equals(base const& other) const {
            return ((std::get<I>(iters) == std::get<I>(other.iters)) || ...);
        }
        void advance(difference_type n){ ((std::get<I>(iters) += n), ...); }
        
        base(): iters() {}
        base(const storage_t& s, auto f): iters(f(std::get<I>(s))...) {}
    };

    struct iterator
        : base<std::make_index_sequence<sizeof...(Range)>>
    {
        using base<std::make_index_sequence<sizeof...(Range)>>::base;
        iterator& operator++() { this->increment(); return *this; }
        iterator  operator++(int) { auto rc(*this); operator++(); return rc; }
        iterator& operator--() { this->decrement(); return *this; }
        iterator  operator--(int) { auto rc(*this); operator--(); return rc; }
        iterator& operator+= (difference_type n) { this->advance(n); return *this; }
        iterator& operator-= (difference_type n) { this->advance(-n); return *this; }
        bool      operator== (iterator const& other) const { return this->equals(other); }
        auto      operator<=> (iterator const& other) const {
            return std::get<0>(this->iters) <=> std::get<0>(other.iters);
        }
        friend iterator operator+ (iterator it, difference_type n) { return it += n; }
        friend iterator operator+ (difference_type n, iterator it) { return it += n; }
        friend iterator operator- (iterator it, difference_type n) { return it -= n; }
        friend difference_type operator- (iterator it0, iterator it1) {
            return std::get<0>(it0.iters) - std::get<0>(it1.iters);
        }
    };

    zip_view(): ranges() {}
    template <typename... R>
    zip_view(R&&... ranges): ranges(std::forward<R>(ranges)...) {}

    iterator begin() const { return iterator(ranges, [](auto& r){ return std::ranges::begin(r); }); }
    iterator end() const { return iterator(ranges, [](auto& r){ return std::ranges::end(r); }); }
};

auto zip(auto&&... ranges)
    -> zip_view<decltype(ranges)...>
{
    return {std::forward<decltype(ranges)>(ranges)...};
}

This implementation makes some decisions about the value_type and the reference type and how to keep track of the different ranges.这个实现对value_typereference类型以及如何跟踪不同的范围做出了一些决定。 Other choices may be more reasonable ( P2214 makes slightly different, probably better, choices).其他选择可能更合理( P2214做出的选择略有不同,可能更好)。 The only tricky bit in this implementation is operating on the std::tuple s which requires a parameter pack containing indices or a suitable set of algorithms on std::tuple s.此实现中唯一棘手的一点是在std::tuple上操作,这需要包含索引的参数包或std::tuple上的一组合适的算法。

With all of that in place a multi-range transform can be used nicely, eg:有了所有这些,就可以很好地使用多范围transform ,例如:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <memory>
#include <ranges>
#include <utility>
#include <tuple>
#include <type_traits>
#include <vector>
    
// zip_view, zip, and transform as above

int main()
{
    std::vector<int> v0{1, 2, 3};
    std::vector<int> v1{4, 5, 6};
    std::vector<int> v2{7, 8, 9};
    for (auto x: transform([](auto a, auto b, auto c){ return a + b + c; }, v0, v1, v2)) {
        std::cout << x << ' ';
    }
    std::cout << '\n';
}

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

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