Ranges And Views: Good Or Bad?¶
Dangling Iterator Protection¶
A range is one object describing a … well … a range of elements
Traditionally (pre-20), described by a pair of iterators
[begin, end)
#include <vector> #include <algorithm> #include <iostream> int main() { auto numbers = {42, 666, -1, 5}; auto iter = std::min_element(numbers.begin(), numbers.end()); std::cout << *iter << std::endl; return 0; }
Ranges do the same, using one single object of range (or view) type
#include <vector> #include <algorithm> // <--- adds to std::ranges namespace #include <iostream> int main() { auto numbers = {42, 666, -1, 5}; auto iter = std::ranges::min_element(numbers); // <--- numbers satisfies the // std::ranges::forward_range concept std::cout << *iter << std::endl; return 0; }
⟶ much more possibilities
For example …
Passing temporaries adds to readability
… but often also to bugginess
#include <vector>
#include <algorithm>
#include <iostream>
int main()
{
auto iter = std::ranges::min_element(
std::vector{42, 666, -1, 5}); // <--- temporary used!
std::cout << *iter << std::endl; // <--- error: no match for ‘operator*’
// (operand type is ‘std::ranges::dangling’)
return 0;
}
std::ranges::dangling
when passing an RValue⟶ compiler error
Performance: views::drop
Is Easy¶
How fast can
.size()
be determined on adrop_view
(the result ofstd::views::drop()
)⟶ It is simply a matter of subtracting 2 from the underlying range’s
.size()
#include <vector>
#include <ranges>
#include <iostream>
int main()
{
std::vector numbers = {2, 1, 4, 3, 5};
auto even = numbers | std::views::drop(2);
std::cout << even.size() << std::endl; // <--- ok: fast
return 0;
}
Performance: views::filter
Is Not Easy¶
How long would
.size()
take on afilter_view
?⟶ Apply filter, and count how many are accepted
⟶ Rewind?
#include <vector>
#include <ranges>
#include <iostream>
int main()
{
std::vector numbers = {2, 1, 4, 3, 5};
auto even = numbers | std::views::filter([](auto elem) { return elem%2==0; });
std::cout << even.size() << std::endl; // <--- compiler error: no size()
return 0;
}
Compiler error, saying that there is no
.size()
on itTrying to say that
.size()
cannot be computed in constant time⟶ “… constraints not satisfied …”
size-filter-nok.cpp:9:27: error: no matching function for call to ‘std::ranges::filter_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:17)> >::size()’
9 | std::cout << even.size() << std::endl; // <--- compiler error
| ~~~~~~~~~^~
In file included from /usr/include/c++/12/ranges:47,
from size-filter-nok.cpp:2:
/usr/include/c++/12/bits/ranges_util.h:131:7: note: candidate: ‘constexpr auto std::ranges::view_interface<_Derived>::size() requires (forward_range<_Derived>) && (sized_sentinel_for<decltype(std::ranges::__cust::end((declval<_Tp&>)())), decltype(std::ranges::__cust_access::__begin((declval<_Container&>)()))>) [with _Derived = std::ranges::filter_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:17)> >]’
131 | size() noexcept(noexcept(_S_size(_M_derived())))
| ^~~~
/usr/include/c++/12/bits/ranges_util.h:131:7: note: constraints not satisfied
Constraints: How To Read?¶
Careful reading …
candidate: ‘constexpr auto std::ranges::view_interface<_Derived>::size() requires (forward_range<_Derived>) && (sized_sentinel_for<...
forward_range
: Can be iterated from beginning to end multiple timessized_sentinel_for
:The sized_sentinel_for concept specifies that an object of theiterator type I and an object of the sentinel type S can be subtractedto compute the distance between them in constant time.⟶ A-ha!
.size()
is not there becausefilter_view
is linear time
And drop()
? std::vector
Is Easy¶
Drop 2 elements means ignoring the first two elements
Cheap with a
std::vector
⟶v.begin() + 2
(simple arithmetic)
#include <vector>
#include <ranges>
#include <iostream>
void print(const auto& coll)
{
for (const auto& elem: coll)
std::cout << elem << std::endl;
}
int main()
{
std::vector numbers = {2, 1, 4, 3, 5};
print(numbers | std::views::drop(2));
return 0;
}
And drop()
? std::list
Is Not Easy¶
Expensive with
std::list
⟶ manually forwarding a linked list to 2nd element
#include <list>
#include <ranges>
#include <iostream>
void print(const auto& coll) // <--- note the const!
{
for (const auto& elem: coll)
std::cout << elem << std::endl;
}
int main()
{
std::list numbers = {2, 1, 4, 3, 5};
print(numbers | std::views::drop(2)); // <--- compiler error
return 0;
}
Compiler error, saying that
print()
cannot be instantiated because …passing ‘const std::ranges::drop_view as ‘this’ argument discards qualifiers
drop-nok.cpp: In instantiation of ‘void print(const auto:17&) [with auto:17 = std::ranges::drop_view<std::ranges::ref_view<std::__cxx11::list<int, std::allocator<int> > > >]’:
drop-nok.cpp:14:10: required from here
drop-nok.cpp:7:5: error: passing ‘const std::ranges::drop_view<std::ranges::ref_view<std::__cxx11::list<int, std::allocator<int> > > >’ as ‘this’ argument discards qualifiers [-fpermissive]
7 | for (const auto& elem: coll)
| ^~~
In file included from drop-nok.cpp:2:
/usr/include/c++/12/ranges:2460:7: note: in call to ‘constexpr auto std::ranges::drop_view<_Vp>::begin() requires !((__simple_view<_Vp>) && (random_access_range<const _Vp>) && (sized_range<const _Vp>)) [with _Vp = std::ranges::ref_view<std::__cxx11::list<int, std::allocator<int> > >]’
2460 | begin()
| ^~~~~
Point is:
drop_view
cannot be passed asconst
because it cachesbegin()
Bullshit
⟶ ALL SORTS OF BUGS!
One Of Many Bugs, Introduced By .begin()
Caching¶
Caching means that
const
is not easily pretendedmutable
is only a lieEverything that I expect does not hold - at least not in any case
In some cases I cannot read-only iterate a
const
rangeIn some cases concurrent read-only iteration is not safe ⟶ race condition in
.begin()
What is the output of the following program?
#include <vector>
#include <ranges>
#include <iostream>
int main()
{
std::vector numbers = {1, 3, 4, 3, 5, 2};
auto even = numbers | std::views::filter([](auto elem) { return elem%2==0; });
for (auto& elem: even)
elem += 1;
for (auto elem: even)
std::cout << elem << std::endl;
return 0;
}
Answer:
$ ./c++11-ranges-modify-view-nok
5
5
is at index[2]
which is cached as ex4
- the first even number in the ex-vector⟶ be careful!
Exceptions From Rules ⟶ Dogmatic Don’ts (Sigh, C++)¶
Don’t re-use views
Don’t modify collections through views
Don’t use multiple threads (not a good idea anyway)
Many more, I’m certain - watch out!
Thanks to Nicolai Josuttis who is bashing the C++ standards comittee in this video. He is much less polite than I am.