1

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.

cpplearner
  • 13,776
  • 2
  • 47
  • 72
SomeProgrammer
  • 1,134
  • 1
  • 6
  • 12
  • 1
    This is a very good question, and I'm trying to figure out the answer, myself. – Sam Varshavchik May 16 '21 at 14:36
  • Ranges can also chain operations. – HolyBlackCat May 16 '21 at 14:36
  • @HolyBlackCat This can be done with iterators too, if you overload the pipe operator. Why did we need to introduce a whole new concept like ranges? – SomeProgrammer May 16 '21 at 14:38
  • 6
    *"can be done with iterators too"* But less conveniently. By the same logic we should be writing in pure C, since it can do anything C++ can, etc. – HolyBlackCat May 16 '21 at 14:43
  • @ HolyBlackCat Why less conveniently? – SomeProgrammer May 16 '21 at 14:47
  • I mean it needs around twice as much typing. – HolyBlackCat May 16 '21 at 14:47
  • @HolyBlackCat No I think you could overload the piping operator so that iterators can chain methods exactly as ranges can. Or is there some limitation of iterators which I don't know about? – SomeProgrammer May 16 '21 at 14:48
  • @SomeProgrammer Assuming the rhs of `|` is an algorithm name, how are going to write two iterators in the lhs? Answer: you need a single class that stores both. Add some fancy template safeguards, and you get ranges. – HolyBlackCat May 16 '21 at 14:51
  • 1
    @SomeProgrammer No, you can't. Because a single iterator is only half of the information. You can't chain iterators. `it | transform(f)` gives you an adapted iterator, but you'd also need to adapt the end iterator in order to be able to do anything. It's not enough to be able to do anything. Imagine also how this looks with like... `filter` – Barry May 16 '21 at 14:52
  • @HolyBlackCat why can't you just take a container on the lhs? – SomeProgrammer May 16 '21 at 14:54
  • But this is exactly what ranges do, no? – HolyBlackCat May 16 '21 at 14:54
  • 2
    @SomeProgrammer Oh, so you're saying you need a range? – Barry May 16 '21 at 14:54
  • @HolyBlackCat Wouldn't ranges need to copy the elements anyway? – SomeProgrammer May 16 '21 at 15:05
  • I'm not *too* familiar with ranges, but I assume they're not doing useless copies. Can you make an example of ranges copying elements unnecessarily? – HolyBlackCat May 16 '21 at 15:07
  • 2
    @SomeProgrammer: "*I think you could overload the piping operator so that iterators can chain methods exactly as ranges can.*" If that's the case, then provide some evidence for it. Put some code in your question as to how chaining and other range operations would work on plain iterators. – Nicol Bolas May 16 '21 at 15:17

2 Answers2

7

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

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
4

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.