Skip to content

Commit 86b77a5

Browse files
committed
[feature] radr::find_common_end customisation point
1 parent 6057dce commit 86b77a5

File tree

11 files changed

+254
-111
lines changed

11 files changed

+254
-111
lines changed

docs/caching_begin.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ We want to find out how many times the predicate is evaluated when using the ada
7171
| `for (size_t i : rad) {}` (2nd+) | 9 |
7272
| `for (size_t i : rad │ radr::take(100)) {}` (comb.) | 9 |
7373
74-
This tables illustrates the differences and similarities between the standard library and our library:
74+
This tables illustrates the similarities between the standard library and our library:
7575
* Constructing the adaptor and iterating over it once adds up to 15 predicate calls.
7676
* The second (and any further) iteration only performs 9 calls.
7777
* Note that since neither `std` nor `radr` cache the end, the *last* five 1s will always be parsed. *More on this below*.
7878
79-
Finally, we see that the libraries differ when the adaptor is combined with another one, e.g. `take(100)`.
79+
They differ in that some of the work happens on construction in RADR.
80+
And finally, we see that the libraries differ when the adaptor is combined with another one, e.g. `take(100)`.
8081
The standard library adaptor resets its cache when being moved or copied, because the caching mechanism in the standard library is *non-propagating*.
8182
This is not known to most users of `std::ranges::`, and it is even more surprising since "building up" ranges pipelines in multiple steps is a frequently recommended practice.
8283
@@ -95,11 +96,11 @@ In our library `radr::filter` is not common by default. This has the advantage t
9596
9697
| `radr::` | # calls |
9798
|------------------------------------------------------------------------------|------------:|
98-
| `auto rad = std::ref(vec) │ radr::filter(even) │ radr::to_common;` (constr.) | 15 |
99+
| `auto rad = std::ref(vec) │ radr::filter(even) │ radr::to_common;` (constr.) | 12 |
99100
| `for (size_t i : rad) {}` (1st) | 4 |
100101
| `for (size_t i : rad) {}` (2nd+) | 4 |
101102
| `for (size_t i : rad │ radr::take(100)) {}` (comb.) | 4 |
102103
103-
As you see, the range is parsed once completely on construction.
104+
The head of the range is parsed on construction of `filter` (6 calls), and the tail is parsed on construction of `to_common` (reverse find, also 6 calls).
104105
After this, the adaptor needs to parse neither the head, nor the tail of the underlying range on a full iteration.
105106
The standard library does not offer such facilities.

docs/getting_started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ We fundamentally differentiate three types of ranges:
4545
4646
Multi-pass ranges can be accessed multiple times while single-pass ranges cannot generally be read again from the beginning.
4747
See [Range properties](./range_properties.md) for formal definitions.
48+
Many types and concepts in the library use the abbreviation `mp` for "multi-pass" or `sp` for "single-pass".
4849
4950
A *range adaptor* is a range that is created on top of another range, typically referred to as the *underlying range* or
5051
the *original range*.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// -*- C++ -*-
2+
//===----------------------------------------------------------------------===//
3+
//
4+
// Copyright (c) 2023-2025 Hannes Hauswedell
5+
//
6+
// Licensed under the Apache License v2.0 with LLVM Exceptions.
7+
// See the LICENSE file for details.
8+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#pragma once
13+
14+
#include <iterator>
15+
16+
#include "tags.hpp"
17+
18+
namespace radr
19+
{
20+
21+
//=============================================================================
22+
// Wrapper function find_common_end
23+
//=============================================================================
24+
25+
struct find_common_end_impl_t
26+
{
27+
//!\brief Call tag_invoke if possible; call default otherwise. [it, sen]
28+
template <typename It, typename Sen>
29+
constexpr It operator()(It b, Sen e) const
30+
{
31+
static_assert(std::forward_iterator<It>,
32+
"You must pass a forward_iterator as first argument to radr::find_common_end.");
33+
static_assert(std::sentinel_for<Sen, It>,
34+
"You must pass a sentinel as second argument to radr::find_common_end.");
35+
static_assert(!std::same_as<Sen, std::unreachable_sentinel_t>,
36+
"You must not pass infinite ranges to radr::find_common_end.");
37+
38+
if constexpr (std::same_as<It, Sen>)
39+
{
40+
return e;
41+
}
42+
else if constexpr (requires { tag_invoke(custom::find_common_end_tag{}, std::move(b), std::move(e)); })
43+
{
44+
using ret_t = decltype(tag_invoke(custom::find_common_end_tag{}, std::move(b), std::move(e)));
45+
static_assert(std::same_as<ret_t, It>,
46+
"Your customisations of radr::find_common_end must always return the iterator type.");
47+
return tag_invoke(custom::find_common_end_tag{}, std::move(b), std::move(e));
48+
}
49+
else
50+
{
51+
auto ret = b;
52+
std::ranges::advance(ret, e);
53+
return ret;
54+
}
55+
}
56+
57+
//!\}
58+
};
59+
60+
/*!\brief Given an iterator-sentinel pair, return an iterator that can be used as the sentinel.
61+
* \param[in] b The iterator.
62+
* \param[in] e The sentinel.
63+
* \returns An iterator that can be used with \p in to represent the same range as `(b, e)` but where iterator and sentinel have the same type.
64+
* \pre `(b, e)` denotes a valid, finite range.
65+
* \details
66+
*
67+
* NOOP if \p b and \p e already have the same type.
68+
*
69+
* ### Complexity
70+
*
71+
* The default implementation is **linear**; the end is searched from the beginning.
72+
*
73+
* ### Customisation
74+
*
75+
* You may provide overloads with the following signature to customise the behaviour:
76+
*
77+
* ```cpp
78+
* It tag_invoke(radr::find_common_end_tag, It, Sen);
79+
* ```
80+
*
81+
* They must be visible to ADL.
82+
*
83+
*/
84+
inline constexpr find_common_end_impl_t find_common_end{};
85+
86+
} // namespace radr

include/radr/custom/subborrow.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ struct subborrow_impl_t
209209
}
210210
}
211211

212+
//!\brief Call tag_invoke if possible (even without size); call default otherwise. [it, sen, not_size]
213+
template <borrowed_mp_range URange, detail::is_iterator_of<URange> It, typename Sen>
214+
constexpr auto operator()(URange && urange, It const b, Sen const e, detail::not_size const) const
215+
{
216+
return operator()(std::forward<URange>(urange), b, e);
217+
}
218+
212219
//!\brief Call tag_invoke if possible; call default otherwise. [i, j]
213220
template <borrowed_mp_range URange>
214221
requires(std::ranges::random_access_range<URange> && std::ranges::sized_range<URange>)

include/radr/custom/tags.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
namespace radr::custom
1515
{
1616

17+
struct rebind_iterator_tag
18+
{};
19+
1720
struct subborrow_tag
1821
{};
1922

20-
struct rebind_iterator_tag
23+
struct find_common_end_tag
2124
{};
2225

2326
} // namespace radr::custom

include/radr/rad/filter.hpp

Lines changed: 91 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "../detail/semiregular_box.hpp"
2424
#include "../generator.hpp"
2525
#include "../range_access.hpp"
26+
#include "radr/custom/tags.hpp"
2627

2728
namespace radr::detail
2829
{
@@ -66,31 +67,78 @@ class filter_iterator
6667
using RIt = filter_iterator<Iter, Iter, Func>;
6768
Iter new_uend = sen.base_iter();
6869

69-
if constexpr (std::bidirectional_iterator<Iter> && !std::same_as<Iter, Sent>)
70+
RIt rit{std::move(it).func(), std::move(it).base_iter(), new_uend};
71+
RIt rsen{std::move(sen).func(), new_uend, new_uend};
72+
return borrowing_rad{std::move(rit), std::move(rsen), s};
73+
}
74+
75+
/*!\brief Customisation for subranges.
76+
* \details
77+
*
78+
* This potentially returns a range of different filter_iterators. In particular, the returned
79+
* ones are always built on iterator-sentinel of the same type.
80+
*/
81+
template <borrowed_mp_range R>
82+
constexpr friend auto tag_invoke(custom::subborrow_tag, R &&, filter_iterator it, filter_iterator sen)
83+
{
84+
return subborrow_impl(it, sen, not_size{});
85+
}
86+
87+
//!\overload
88+
template <borrowed_mp_range R>
89+
constexpr friend auto tag_invoke(custom::subborrow_tag,
90+
R &&,
91+
filter_iterator it,
92+
filter_iterator sen,
93+
size_t const s)
94+
{
95+
return subborrow_impl(it, sen, s);
96+
}
97+
98+
//!\brief Customisation to create common sentinel with actual underlying end.
99+
constexpr friend filter_iterator tag_invoke(custom::find_common_end_tag,
100+
filter_iterator it,
101+
std::default_sentinel_t)
102+
{
103+
Iter ubeg = std::move(it).base_iter();
104+
Sent uend = std::move(it).base_sent();
105+
Func fn = std::move(it).func();
106+
107+
Iter it_end{};
108+
109+
/* search backwards from end if possible */
110+
if constexpr (std::bidirectional_iterator<Iter> && std::same_as<Iter, Sent>)
70111
{
71-
if (it != sen)
112+
it_end = uend;
113+
while (it_end != ubeg)
72114
{
73-
/* For filter iterators on non-common ranges, sen might not represent the "real"
74-
* underlying end, because the "caching mechanism" implemented in operator++
75-
* does not take effect.
76-
* For these ranges, we do the tiny hack of creating a "filter_iterator-on-common",
77-
* and then we decrement which will move onto the last valid underlying element.
78-
* The we increment that to get the real underlying past-the-end iterator.
79-
*
80-
* A cleaner solution would be to introduce a `to_common` customisation point.
81-
* This would allow avoiding the back and forth.
82-
* We might do this in the future!
83-
*/
84-
RIt rsen{sen.func(), new_uend, new_uend};
85-
--rsen;
86-
new_uend = std::move(rsen).base_iter();
87-
++new_uend;
115+
--it_end;
116+
if (std::invoke(std::cref(fn), *it_end))
117+
{
118+
++it_end;
119+
break;
120+
}
88121
}
89122
}
123+
/* search from beginning but store element behind last matching instead of underlying end */
124+
else
125+
{
126+
bool empty = true;
90127

91-
RIt rit{std::move(it).func(), std::move(it).base_iter(), new_uend};
92-
RIt rsen{std::move(sen).func(), new_uend, new_uend};
93-
return borrowing_rad{std::move(rit), std::move(rsen), s};
128+
for (auto it = ubeg; it != uend; ++it)
129+
{
130+
if (std::invoke(std::cref(fn), *it))
131+
{
132+
it_end = it;
133+
empty = false;
134+
}
135+
}
136+
137+
if (!empty)
138+
++it_end;
139+
}
140+
141+
return {std::move(fn), std::move(it_end), std::move(uend)};
94142
}
95143

96144
public:
@@ -126,17 +174,8 @@ class filter_iterator
126174

127175
constexpr filter_iterator & operator++()
128176
{
129-
if constexpr (std::same_as<Iter, Sent>)
130-
{
131-
if (auto tmp = std::ranges::find_if(++current_, end_, std::ref(*func_)); tmp != end_)
132-
current_ = std::move(tmp);
133-
else // cache the real underlying end
134-
end_ = current_;
135-
}
136-
else
137-
{
138-
current_ = std::ranges::find_if(std::move(++current_), end_, std::ref(*func_));
139-
}
177+
current_ = std::ranges::find_if(std::move(++current_), end_, std::cref(*func_));
178+
140179
return *this;
141180
}
142181

@@ -187,29 +226,6 @@ class filter_iterator
187226
{
188227
std::ranges::iter_swap(x.current_, y.current_);
189228
}
190-
191-
/*!\brief Customisation for subranges.
192-
* \details
193-
*
194-
* This potentially returns a range of different filter_iterators. In particular, the returned
195-
* ones are always built on iterator-sentinel of the same type.
196-
*/
197-
template <borrowed_mp_range R>
198-
constexpr friend auto tag_invoke(custom::subborrow_tag, R &&, filter_iterator it, filter_iterator sen)
199-
{
200-
return subborrow_impl(it, sen, not_size{});
201-
}
202-
203-
//!\overload
204-
template <borrowed_mp_range R>
205-
constexpr friend auto tag_invoke(custom::subborrow_tag,
206-
R &&,
207-
filter_iterator it,
208-
filter_iterator sen,
209-
size_t const s)
210-
{
211-
return subborrow_impl(it, sen, s);
212-
}
213229
};
214230

215231
// iterator-based borrow
@@ -291,21 +307,29 @@ inline namespace cpo
291307
* * radr::constant_range
292308
*
293309
* It does not preserve:
294-
* * std::ranges::sized_range
295-
*
296-
* **In contrast to std::views::filter, it DOES NOT PRESERVE:**
310+
* * std::ranges::sized_range
297311
* * radr::common_range
298312
* * radr::mutable_range
299313
*
300314
* To prevent UB, the returned range is always a radr::constant_range.
301315
*
302-
* Use `radr::filter(Fn) | radr::to_common` to make the range common, and use
303-
* `radr::to_single_pass | radr::filter(Fn)`, create a mutable range (see below).
304-
*
305316
* Construction of the adaptor is in O(n), because the first matching element is searched and cached.
306317
*
307318
* Multiple nested filter adaptors are folded into one.
308319
*
320+
* ### Notable differences to std::views::filter
321+
*
322+
* Like all our multi-pass adaptors (but unlike std::views::filter), this adaptor is const-iterable.
323+
*
324+
* Does not preserve radr::common_range. Use `… | radr::filter(Fn) | radr::to_common` if you want a common range.
325+
* This has the added benefit that the real underlying end is used and the tail of the underlying range is not searched repeatedly.
326+
* See [caching end](docs/caching_begin.md#caching-end) for more details.
327+
*
328+
* Does not preserve radr::mutable_range, i.e. it always returns radr::constant_range.
329+
* This prevents undefined behaviour by accidentally changing values that invalidate the predicate.
330+
* If you want a mutable range, use the single pass adaptor like this:
331+
* `… | radr::to_single_pass | radr::filter(fn)`
332+
*
309333
* ## Single-pass ranges
310334
*
311335
* Requirements:
@@ -315,6 +339,13 @@ inline namespace cpo
315339
* The single-pass version of this adaptor preserves mutability, i.e. it allows changes to the underlying range's elements.
316340
* It also allows (observable) changes in the predicate.
317341
*
342+
* ### Notable differences to std::views::filter
343+
*
344+
* In C++26 the single-pass version of std::views::filter was made const-iterable (but not the multi-pass version).
345+
* This is counter to any consistency in the range concepts.
346+
*
347+
* All our multi-pass adaptors are const-iterable, but non of our single-pass adaptors are.
348+
*
318349
*/
319350
inline constexpr auto filter = detail::pipe_with_args_fn{detail::filter_coro, detail::filter_borrow};
320351
} // namespace cpo

0 commit comments

Comments
 (0)