7

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 algoirthms 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';
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • I would say zip_view first, but not C++20. – Jarod42 Dec 25 '20 at 18:36
  • There is no such function or capability in C++20. – Nicol Bolas Dec 25 '20 at 18:37
  • @Jarod42: yes, ranges-v3 does contain `zip()` but C++20 doesn't. I think it is still the answer but it will take me a moment to punch in `zip()` :) – Dietmar Kühl Dec 25 '20 at 18:37
  • Can you show an example usage of the 2 input range version of `transform` that you would like to write? – cigien Dec 25 '20 at 18:47
  • @cigien: I would like to replace the non-ranges version by one using ranges, e.g., to avoid creating a temporary range. That could look like so (I have swapped the function argument to the front as that makes things a bit easier to implement for an arbitrary number of ranges): for (auto v: envisioned::transform([](auto a, auto b){ return a + b; }, v0, v1)){ std::cout << v << ' '; }`. Inside `envisioned::transform()` I would envision using `std::ranges::views::transform()`. – Dietmar Kühl Dec 25 '20 at 18:51
  • An *arbitrary* number of ranges can't be handled, but you can use 2 input ranges. The specific example you've shown in the comment is implementable with C++20 ranges. If that's sufficient, add that to the question, and I'll post an answer. – cigien Dec 25 '20 at 18:55
  • @cigien: I have update my question. I do believe a solution with an arbitrary number of ranges (well, there is probably some technical limitation but the conceptually it could be arbitrary) is actually possible. However, a replacement of the two sequences supported by `std::transform()` would bring things on par with that version of `transform()`. – Dietmar Kühl Dec 25 '20 at 19:00
  • I've added an answer that address at least the 2 input version in C++20. – cigien Dec 25 '20 at 19:06

3 Answers3

10

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Barry
  • 286,269
  • 29
  • 621
  • 977
  • That's actually interesting: the `zip_with()` transformation is what would build a resulting `std::tuple` if a `tuple`-like elements is desired. However, different ways to combine the sequences are supported, too. Seems that is really a form of `transform()` under a different name: I guess, I prefer `zip_transform` (but let's see what LEWG makes out of that). – Dietmar Kühl Dec 25 '20 at 19:07
  • I was just typing my own answer but seeing one from someone who actually wrote P2214 made me instantly delete it. P1035R5 mentions that `zip_view` was removed as per request from the LEWG, but I could not find any information on what that request was based on. Do you have any information on that? – mrks Dec 25 '20 at 19:09
  • @DietmarKühl Right. If you just want a tuple, you use `zip`. But if you want to apply a function, it's just a more direct way of doing that -- there's no intermediate tuple that gets built up or anything, just `std::invoke(f, *it...)` – Barry Dec 25 '20 at 20:16
  • 1
    @mrks Read the two sections in the paper about making `tuple` `readable` and `writable`. – Barry Dec 25 '20 at 20:17
10

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.

cigien
  • 57,834
  • 11
  • 73
  • 112
  • I'm not seeing `std::ranges::transform` which takes two ranges, though. There is (in [range.transform]) `std::ranges::views::transform` but I don't think this algorithm (view factory) takes two ranges. Do you have a reference to version you are using? – Dietmar Kühl Dec 25 '20 at 19:14
  • @DietmarKühl The last overload [here](https://eel.is/c++draft/alg.transform) is the correct one, and it does [work](https://godbolt.org/z/vf4oax). Would you like me to add the demo and/or reference to the answer? – cigien Dec 25 '20 at 19:17
  • I just tried it out with `g++` and it, indeed, works. It may be an extension in `libstdc++`. – Dietmar Kühl Dec 25 '20 at 19:24
  • @DietmarKühl Ah, ok then. I added the demo and the reference to the answer anyway :) – cigien Dec 25 '20 at 19:25
  • You are right: these versions are, indeed, defined. ... with a somewhat interesting way of defining them and in the `` header rather than the `` header. Thanks! – Dietmar Kühl Dec 25 '20 at 19:30
1

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';
}
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 1
    There's a few issues with this implementation of `zip`. For some inputs, it fails to meet the semantics of `view` (`zip(std::move(v0))` isn't O(1) copyable). For others it doesn't work (like `zip(as_const(v0))` because the reference type is wrong). Also, `zip` could be random access. – Barry Dec 25 '20 at 23:19
  • @Barry: Thanks for pointing out the issues! I think I have addressed those you mentioned. The code certainly isn't thoroughly tested and there are probably other issues. – Dietmar Kühl Dec 26 '20 at 00:19