Reputation: 1191
I have been trying to understand the new ranges library and try to convert some of the more traditional for loops into functional code. The example code given by cppreference is very straight forward and readable. However, I am unsure how to apply Ranges over a vector of Points that needs to have every x and y values looked at, calculated, and compared at the end for which is the greatest distance.
struct Point
{
double x;
double y;
}
double ComputeDistance(const Point& p1, const Point& p2)
{
return std::hypot(p1.x - p2.x, p1.y - p2.y);
}
double GetMaxDistance(const std::vector<Point>& points)
{
double maxDistance = 0.0;
for (int i = 0; i < points.size(); ++i)
{
for(int j = i; j < points.size(); ++j)
{
maxDistance = std::max(maxDistance, ComputeDistance(points.at(i),points.at(j)));
}
}
return maxDistance;
}
GetMaxDistance
is the code that I would love to try and clean up and apply ranges on it. Which I thought would be as simple as doing something like:
double GetMaxDistance(const std::vector<Point>& points)
{
auto result = points | std::views::tranform(ComputeDistance);
return static_cast<double>(result);
}
And then I realized that was not correct since I am not passing any values into the function. So I thought:
double GetMaxDistance(const std::vector<Point>& points)
{
for(auto point : points | std::views::transform(ComputeDistance))
// get the max distance somehow and return it?
// Do I add another for(auto nextPoint : points) here and drop the first item?
}
But then I realized that I am applying that function to every point, but not the point next to it, and this would also not work since I am still only passing in one argument into the function ComputeDistance
. And since I need to compute the distance of all points in the vector I have to compare each of the points to each other and do the calculation. Leaving it as an n^2
algorithm. Which I am not trying to beat n^2
, I would just like to know if there is a way to make this traditional for loop take on a modern, functional approach.
Which brings us back to the title. How do I apply std::ranges
in this case? Is it even possible to do with what the standard has given us at this point? I know more is to be added in C++23. So I don't know if this cannot be achieved until that releases or if this is not possible to do at all.
Thanks!
Upvotes: 1
Views: 1861
Reputation: 303067
The algorithm you're looking for is combinations - but there's no range adaptor for that (neither in C++20 nor range-v3 nor will be in C++23).
However, we can manually construct it in this case using an algorithm usually called flat-map:
inline constexpr auto flat_map = [](auto f){
return std::views::transform(f) | std::views::join;
};
which we can use as follows:
double GetMaxDistance(const std::vector<Point>& points)
{
namespace rv = std::views;
return std::ranges::max(
rv::iota(0u, points.size())
| flat_map([&](size_t i){
return rv::iota(i+1, points.size())
| rv::transform([&](size_t j){
return ComputeDistance(points[i], points[j]);
});
}));
}
The outer iota
is our first loop. And then for each i
, we get a sequence from i+1
onwards to get our j
. And then for each (i,j)
we calculate ComputeDistance
.
Or if you want the transform
at top level (arguably cleaner):
double GetMaxDistance(const std::vector<Point>& points)
{
namespace rv = std::views;
return std::ranges::max(
rv::iota(0u, points.size())
| flat_map([&](size_t i){
return rv::iota(i+1, points.size())
| rv::transform([&](size_t j){
return std::pair(i, j);
});
})
| rv::transform([&](auto p){
return ComputeDistance(points[p.first], points[p.second]);
}));
}
or even (this version produces a range of pairs of references to Point
, to allow a more direct transform
):
double GetMaxDistance(const std::vector<Point>& points)
{
namespace rv = std::views;
namespace hof = boost::hof;
return std::ranges::max(
rv::iota(0u, points.size())
| flat_map([&](size_t i){
return rv::iota(i+1, points.size())
| rv::transform([&](size_t j){
return std::make_pair(
std::ref(points[i]),
std::ref(points[j]));
});
})
| rv::transform(hof::unpack(ComputeDistance)));
}
These all basically do the same thing, it's just a question of where and how the ComputeDistance
function is called.
C++23 will add cartesian_product
and chunk
(range-v3 has them now) , and just recently added zip_transform
, which also will allow:
double GetMaxDistance(const std::vector<Point>& points)
{
namespace rv = std::views;
namespace hof = boost::hof;
return std::ranges::max(
rv::zip_transform(
rv::drop,
rv::cartesian_product(points, points)
| rv::chunk(points.size()),
rv::iota(1))
| rv::join
| rv::transform(hof::unpack(ComputeDistance))
);
}
cartesian_product
by itself would give you all pairs - which both includes (x, x)
for all x
and both (x, y)
and (y, x)
, neither of which you want. When we chunk it by points.size()
(produces N
ranges of length N
), then we repeatedly drop a steadingly increasing (iota(1)
) number of elements... so just one from the first chunk (the pair that contains the first element twice) and then two from the second chunk (the (points[1], points[0])
and (points[1], points[1])
elements), etc.
The zip_transform
part still produces a range of chunks of pairs of Point
, the join
reduces it to a range of pairs of Point
, which we then need to unpack
into ComputeDistance
.
This all exists in range-v3 (except zip_transform
there is named zip_with
). In range-v3 though, you get common_tuple
, which Boost.HOF doesn't support, but you can make it work.
Upvotes: 10