more elaborate range tutorial
parent
fe4d7a553a
commit
1c0997cdc4
297
doc/ranges.md
297
doc/ranges.md
|
@ -18,6 +18,17 @@ A range is a type that represents an interval of values. Just like with C++
|
|||
iterators, there are several categories of ranges, with each enhancing the
|
||||
previous in some way.
|
||||
|
||||
You can use ranges with custom algorithms or standard algorithms that are
|
||||
implemented by OctaSTD. You can also iterate any input-type range directly
|
||||
using the range-based for loop:
|
||||
|
||||
~~~{.cc}
|
||||
my_range r = some_range;
|
||||
for (range_reference_t<my_range> v: r) {
|
||||
// done for each item of the range
|
||||
}
|
||||
~~~
|
||||
|
||||
## Implementing a range
|
||||
|
||||
Generally, there are two kinds of ranges, *input ranges* and *output ranges*.
|
||||
|
@ -47,10 +58,10 @@ input range meet the requirements for an output range. These are called
|
|||
bool empty() const;
|
||||
void pop_front();
|
||||
reference front() const;
|
||||
|
||||
// optional methods with fallbacks
|
||||
void pop_front_n(size_type n);
|
||||
};
|
||||
|
||||
// optional
|
||||
range_size_t<my_range> range_pop_front_n(my_range &range, range_size_t<my_range> n);
|
||||
~~~
|
||||
|
||||
This is what any input range is required to contain. An input range is the
|
||||
|
@ -76,10 +87,10 @@ But let's take a look at the structure first.
|
|||
|
||||
Any input range (forward, bidirectional etc too!) is required to derive
|
||||
from ostd::input\_range like this. The type provides various convenience
|
||||
methods as well as fallbacks for optional methods. Please refer to its
|
||||
documentation for more information. Keep in mind that none of the provided
|
||||
methods are `virtual`, so it's not safe to call them while expecting the
|
||||
overridden variants to be called.
|
||||
methods as well as some core implementations necessary for the ranges to
|
||||
work. Please refer to its documentation for more information. Keep in mind
|
||||
that none of the provided methods are `virtual`, so it's not safe to call
|
||||
them while expecting the overridden variants to be called.
|
||||
|
||||
~~~{.cc}
|
||||
using range_category = ostd::input_range_tag;
|
||||
|
@ -159,13 +170,16 @@ house and kill your cat. Safe algorithms always need to check for emptiness
|
|||
first.
|
||||
|
||||
~~~{.cc}
|
||||
void pop_front_n(size_type n);
|
||||
range_size_t<my_range> range_pop_front_n(my_range &range, range_size_t<my_range> n);
|
||||
~~~
|
||||
|
||||
There is one more, which is optional. This pops `n` values from the range.
|
||||
It has a default implementation in ostd::input\_range which merely calls the
|
||||
`pop_front()` method `n` times. Custom range types are allowed to override
|
||||
this with their own more efficient implementations.
|
||||
There is one more, which is optional. This pops at most `n` values from the
|
||||
range, simply going over the range and popping out elements until `n` is
|
||||
reached or until the range is empty. The actual number of popped out items
|
||||
is then returned. The default implementation uses slicing for sliceable
|
||||
ranges, so it will be optimal for most of those. For other ranges, it just
|
||||
uses a simple loop. This will work universally, but might not always be
|
||||
the fastest.
|
||||
|
||||
### Output ranges
|
||||
|
||||
|
@ -252,3 +266,262 @@ work with, in many cases it would be otherwise very difficult to handle the
|
|||
errors. Also, it makes it easy to never have to handle any errors, simply
|
||||
by using output ranges backed by unbounded containers, for example
|
||||
ostd::appender\_range.
|
||||
|
||||
### Forward ranges
|
||||
|
||||
Forward ranges extend input ranges. Their category tag type is obviously
|
||||
ostd::forward\_range\_tag. They don't really extend the interface at all
|
||||
compared to plain input ranges. What they do instead is provide an extra
|
||||
guarantee - **all copies of forward ranges have their own state**,
|
||||
which means changes done in one forward range will never reflect in the
|
||||
other forward ranges, even if they're copies of the range. That makes forward
|
||||
ranges suitable for multi-pass algorithms, unlike plain input ranges.
|
||||
|
||||
### Bidirectional ranges
|
||||
|
||||
Bidirectional ranges extend forward ranges. Their category tag type is again
|
||||
pretty obvious, ostd::bidirectional\_range\_tag. They introduce some new
|
||||
methods the type has to satisfy to qualify as a bidirectional range.
|
||||
|
||||
~~~{.cc}
|
||||
void pop_back();
|
||||
reference back() const;
|
||||
~~~
|
||||
|
||||
Bidirectional ranges are accessible from both sides. Therefore, the new methods
|
||||
allow you to pop out an item on the other side as well as retrieve it. The
|
||||
actual behavior of those methods is exactly the same, besides that they work
|
||||
on the other side of the range.
|
||||
|
||||
~~~{.cc}
|
||||
range_size_t<my_range> range_pop_back_n(my_range &range, range_size_t<my_range> n);
|
||||
~~~
|
||||
|
||||
Obviously, as you could implement this optional standalone method previously
|
||||
for the front part of the range, you can now implement it for the back. The
|
||||
details for the default implementation are the same; it uses slicing for
|
||||
ranges that support it and otherwise just a simple loop.
|
||||
|
||||
### Infinite random access ranges
|
||||
|
||||
Infinite random access ranges are ranges that can be indexed at arbitrary
|
||||
points but do not have a known size. They extend bidirectional ranges, and
|
||||
their tag is ostd::random\_access\_range\_tag. The interface extension is
|
||||
very simple:
|
||||
|
||||
~~~{.cc}
|
||||
reference operator[](size_type idx) const;
|
||||
~~~
|
||||
|
||||
This does exactly what it looks like. When indexing ranges, the index has
|
||||
to be positive, hence `size_type`. It returns a `reference`, just like front
|
||||
or back accessors.
|
||||
|
||||
### Finite random access ranges
|
||||
|
||||
Those extend infinite random access ranges. Their category tag is
|
||||
ostd::finite\_random\_access\_range\_tag (duh). They extend thee range
|
||||
interface some more.
|
||||
|
||||
~~~{.cc}
|
||||
size_type size() const;
|
||||
~~~
|
||||
|
||||
You can check how many items are in a finite random access range. That's
|
||||
not the only thing, you can additionally slice them, with this method:
|
||||
|
||||
~~~{.cc}
|
||||
my_range slice(size_type start, size_type end) const;
|
||||
~~~
|
||||
|
||||
Making a slice of a range means creating a new range that contains a subset
|
||||
of the original range's elements. The first provided argument is the first
|
||||
index included in the new range. The second argument is the index past the
|
||||
last index included in the range. Therefore, doing
|
||||
|
||||
~~~{.cc}
|
||||
r.slice(0, r.size());
|
||||
~~~
|
||||
|
||||
returns the entire range, or well, a copy of it. Doing something like
|
||||
|
||||
~~~{.cc}
|
||||
r.slice(1, r.size() - 1);
|
||||
~~~
|
||||
|
||||
will return a range that contains everything but the first or the last items,
|
||||
provided that the range contains at very least 2 items, otherwise the behavior
|
||||
is undefined.
|
||||
|
||||
The slicing indexes follow a regular half-open interval approach, so there
|
||||
shouldn't be anything unclear about it.
|
||||
|
||||
### Contiguous ranges
|
||||
|
||||
Ranges that are contiguous have the ostd::contiguous\_range\_tag. They're like
|
||||
finite random access ranges, except they're guaranteed to back a contiguous
|
||||
block of memory. With contiguous ranges, the following assumptions are true:
|
||||
|
||||
~~~{.cc}
|
||||
// contiguous storage
|
||||
(&range[range.size()] - &range[0]) == range.size()
|
||||
// front item always points to the beginning of the storage
|
||||
&range[0] == &range.front()
|
||||
// back item always points to the end of the storage (but not past)
|
||||
&range[range.size() - 1] == &range.back()
|
||||
~~~
|
||||
|
||||
Contiguous ranges also define extra methods to let you access the internal
|
||||
buffer of the range directly:
|
||||
|
||||
~~~{.cc}
|
||||
value_type *data();
|
||||
value_type const *data() const;
|
||||
~~~
|
||||
|
||||
The meaning is obvious. They're always equivalent to `&range[0]` or
|
||||
`&range.front()`.
|
||||
|
||||
## Range metaprogramming
|
||||
|
||||
There are useful tricks you can use when working with ranges and specializing
|
||||
your algorithms.
|
||||
|
||||
### Wrapper ranges
|
||||
|
||||
Sometimes ranges need to act as wrappers for other ranges. In those cases, you
|
||||
typically want to expose a similar set of functionality as the range you are
|
||||
wrapping. So you define your category simply as
|
||||
|
||||
~~~{.cc}
|
||||
using range_category = range_category_t<Wrapped>;
|
||||
~~~
|
||||
|
||||
Sometimes you can't implement all of the functionality though. What if you
|
||||
want *at most* some category, but always less if the wrapped range does not
|
||||
support it? For example, your wrapped range will always be at most bidirectional,
|
||||
but if the wrapped range is forward it will be forward, and if it's input it
|
||||
will be input. You can do that simply thanks to inheritance of category tags.
|
||||
|
||||
~~~{.cc}
|
||||
using range_category = std::common_type_t<
|
||||
range_category_t<Wrapped>, ostd::bidirectional_range_tag
|
||||
>;
|
||||
~~~
|
||||
|
||||
If the wrapped range is for example random access, ostd::random\_access\_range\_tag
|
||||
inherits from ostd::bidirectional\_range\_tag which is the common type for
|
||||
random access range tag and bidirectional. But if it's just forward, the
|
||||
common type for forward range tag and bidirectional range tag is obviously
|
||||
just forward, as bidirectional inherits from it.
|
||||
|
||||
If you're wrapping multiple ranges and you want the capabilities of your
|
||||
wrapper range to be the same as the common capabilities of all ranges you
|
||||
are wrapping, you can do the same. The std::common_type_t trait takes a
|
||||
variable number of type parameters.
|
||||
|
||||
### Category checks in algorithms
|
||||
|
||||
Consider you're implementing an algorithm which has a generic implementation
|
||||
that works for all range categories but also a more optimal implementation
|
||||
that works as long as your range is at least bidirectional. Checking by using
|
||||
std::is\_same\_v doesn't quite cut it, because that will potentially disregard
|
||||
"better than" bidirectional ranges, even though the algorithm is still valid
|
||||
for those. You could do this:
|
||||
|
||||
~~~{.cc}
|
||||
if constexpr(std::is_convertible_v<
|
||||
range_category_t<my_range>, ostd::bidirectional_range_tag
|
||||
>) {
|
||||
// your more optimal algorithm implementation for bidir and better
|
||||
} else {
|
||||
// generic version for input and forward
|
||||
}
|
||||
~~~
|
||||
|
||||
This works again thanks to tag inheritance. Any tag better than bidirectional
|
||||
is convertible to bidirectional, because they inherit from it, directly or
|
||||
indirectly. But it still feels unwieldy. Fortunately, custom traits are
|
||||
provided by the range system.
|
||||
|
||||
~~~{.cc}
|
||||
if constexpr(ostd::is_bidirectional_range<my_range>) {
|
||||
// your more optimal algorithm implementation for bidir and better
|
||||
} else {
|
||||
// generic version for input and forward
|
||||
}
|
||||
~~~
|
||||
|
||||
These break down to the same thing. Keep in mind that these never fail
|
||||
to expand, so for non-range types they will become `false`.
|
||||
|
||||
#### Dealing with output ranges
|
||||
|
||||
I already mentioned above that input ranges can implement the output range
|
||||
interface and become *mutable ranges*. That's why ostd::is\_output\_range
|
||||
does not only check the category, but also checks the actual capabilities
|
||||
of the range. So if it's possible to work with the range as with an output
|
||||
range (it implements the `.put(v)` method) it will still pass as an output
|
||||
range despite not having the category.
|
||||
|
||||
## Chainable algorithms
|
||||
|
||||
Input ranges provide support for implementing chainable algorithms. Consider
|
||||
you have the following:
|
||||
|
||||
~~~{.cc}
|
||||
template<typename R, typename T>
|
||||
void my_generic_algorithm(R range, T arg) {
|
||||
// implementation
|
||||
}
|
||||
~~~
|
||||
|
||||
You should typically also implement a chainable version. That's done by
|
||||
returning a lambda:
|
||||
|
||||
~~~{.cc}
|
||||
template<typename T>
|
||||
void my_generic_algorithm(T &&arg) {
|
||||
return [arg = std::forward<T>(arg)](auto &&range) {
|
||||
return my_generic_algorithm(
|
||||
std::forward<decltype(range)>(range),
|
||||
std::forward<T>(arg)
|
||||
);
|
||||
};
|
||||
}
|
||||
~~~
|
||||
|
||||
Then you can do either this:
|
||||
|
||||
~~~{.cc}
|
||||
my_generic_algorithm(range, arg);
|
||||
~~~
|
||||
|
||||
or you can do this:
|
||||
|
||||
~~~{.cc}
|
||||
range | my_generic_algorithm(arg)
|
||||
~~~
|
||||
|
||||
This allows you to do longer chains much more readably. Instead of
|
||||
|
||||
~~~{.cc}
|
||||
foo(bar(baz(range, arg1), arg2), arg3)
|
||||
~~~
|
||||
|
||||
you can simply write
|
||||
|
||||
~~~{.cc}
|
||||
baz(range, arg1) | bar(arg2) | foo(arg3)
|
||||
~~~
|
||||
|
||||
This works thanks to ostd::input\_range having the right `|` operator
|
||||
overloads. The implementation of the chainable algorithm still has to
|
||||
be done manually though.
|
||||
|
||||
## More on ranges
|
||||
|
||||
This is not a comprehensive guide to the range API. You will have to check
|
||||
out the actual API documentation for that, start with [range.hh](@ref range.hh).
|
||||
There are many predefined range types provided by that module as well as
|
||||
various other APIs.
|
||||
|
|
Loading…
Reference in New Issue