Reputation: 153830
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
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
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:
std::tuple
, probably retaining the value category of the respective values.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::tuple
s which requires a parameter pack containing indices or a suitable set of algorithms on std::tuple
s.
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
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