Dietmar Kühl
Dietmar Kühl

Reputation: 153830

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, e.g.:

#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 algorithms which does include std::ranges::views::transform(R, F) and its implementation std::ranges::views::transform_view. I can see how to use this transform() with one range, e.g.:

#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? 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). 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):

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

Upvotes: 12

Views: 11948

Answers (3)

cigien
cigien

Reputation: 60218

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. 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:

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.

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; they can be printed as they're generated.

Upvotes: 12

Dietmar K&#252;hl
Dietmar K&#252;hl

Reputation: 153830

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!

It seems there are essentially two ingredients to using std::ranges::views::transform() with multiple ranges:

  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.
  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.

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:

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:

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. Other choices may be more reasonable (P2214 makes slightly different, probably better, choices). The only tricky bit in this implementation is operating on the std::tuples which requires a parameter pack containing indices or a suitable set of algorithms on std::tuples.

With all of that in place a multi-range transform can be used nicely, e.g.:

#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';
}

Upvotes: 1

Barry
Barry

Reputation: 302862

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. There is no such algorithm in C++20.

Until then, the range-v3 version is exactly your use-case:

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.

Upvotes: 12

Related Questions