I struggle to understand what c++20 ranges add compared to good old fashioned iterators. Yes, I guess there is no need to use begin and end anymore, but simple overloads such as:
namespace std {
template <typename Container>
auto transform(Container&& container, auto&&... args) requires ( requires {container.begin(); container.end(); }) {
return transform(container.begin(), container.end(), args...);
}
}
would solve that problem.
Why are ranges useful and when should I use them compared to iterators?
EDIT: I know that ranges have other advantages over iterators (chaining, better methods etc...). However, these (I think?) can all be done with iterators and I don't understand why there was the need to introduce a whole new concept like ranges.
You have argued against your own conclusion, as evidenced here:
template <typename Container>
auto transform(Container&& container, auto&&... args)
requires ( requires {container.begin(); container.end(); }) {
So... what is this? It's a function which takes a template parameter that satisfies a constraint. Let's ignore that this constraint requires member begin/end instead of the more reasonable std::ranges::begin/end requirements.
How many functions are you going to apply this requirement to? Probably lots. Every algorithm is going to have a version that has this requirement on it. So that's starting to look less like a one-off requirement and more like something that should be a named concept.
Especially since that concept should probably specify what kind of iterator the algorithm requires. You don't just need member begin/end; you need them to return an input_or_output_iterator and a sentinel_for that iterator:
requires ( requires(Container c)
{
{c.begin()} -> input_or_output_iterator;
{c.end()} -> sentinel_for<decltype(c.begin())>;
})
Do you really want to type this every time you ask for a "container"? Of course not; that's what named concepts are for.
So what is that concept? It's a thing over which you can iterate, a sequence of values that is accessible through a particular iterator interface.
And the name of that concept should probably be chosen so as not to imply ownership of the sequence of elements. The transform algorithm doesn't care if what it is given owns the sequence or not. So "container" is absolutely the wrong name.
So let's call this concept a rangevalue-sequence. Value-sequences can be iterated over through an iterator/sentinel interface. And you'll probably need to have different categories of value-sequences. Input sequences, forward sequences, contiguous sequences, etc. You maybe want to detect whether the sequence can compute a size in constant time, or whether the sequence is bounded or borrowed from its owner.
And wouldn't it be neat if you could write operators that create views of these value-sequences?
A range by any other name smells just as sweet. Once you start down the dark path of pairing iterators with sentinels, forever will it dominate your destiny.
Ranges are a natural concept when dealing with iterators. And everything built off of the range concepts is an outgrowth of that.
The point of most standard library core concepts, such as iterators, is to unify abstractions which are commonly made in the standard library. For iterators, this implies providing an interface for the commonly used concept of 'this points to an element in a container, and we want to be able to iterate over the container'.
The point of ranges then, is to hide the raw iterators from the public user interface. Very often when iterating, we need 2 pointers; both the start and end of our container. Ranges try to simplify this by hiding this interface, and providing a single interface for functions that operate on all between begin() and end().
In particular, range views would be the main reason to use ranges. They allow for easier to read code when you want to do function composition. The example from cppreference is a good example use:
#include <ranges>
#include <iostream>
int main()
{
auto const ints = {0,1,2,3,4,5};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };
// "pipe" syntax of composing the views:
for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
std::cout << i << ' ';
}
std::cout << '\n';
// a traditional "functional" composing syntax:
for (int i : std::views::transform(std::views::filter(ints, even), square)) {
std::cout << i << ' ';
}
}
When compared to raw iterators, ranges and views provide a higher-level abstraction layer at close to zero costs. In my personal opinion, in particular the 'view piping' is easier to read and more maintainable compared to the code one would write using c++ without ranges.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With