Description
This is a tracking issue for the methods Iterator::advance_by
and DoubleEndedIterator::advance_back_by
.
The feature gate for the issue is #![feature(iter_advance_by)]
.
Previously the recommendation was to implement nth
and nth_back
on your iterators to efficiently advance them by multiple elements at once (useful for .skip(n)
and .step_by(n)
). After this feature is stabilized the recommendation will/should be to implement advance_by
and advance_back_by
instead, because they compose better and are often simpler to implement.
Iterators in libcore that wrap another iterator (possibly from elsewhere than libcore) will need to keep their nth
and nth_back
implementations for the foreseeable future and perhaps indefinitely, because the inner iterator may have an efficient nth
implementation without implementing advance_by
as well.
About tracking issues
Tracking issues are used to record the overall progress of implementation.
They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
Implementation history
- Implementation (Add Iterator::advance_by and DoubleEndedIterator::advance_back_by #76909)
- Add tracking issue (Add tracking issue of iter_advance_by feature #77405)
- Implement for
Chain
(Implement advance_by, advance_back_by for iter::Chain #77594) - implement for
slice::{Iter, IterMut}
(Implement advance_by, advance_back_by for slice::{Iter, IterMut} #87387, #[inline] slice::Iter::advance_by #87736) - implement for
vec::IntoIter, ops::Range, iter::{Cycle, Skip, Take, Copied, Flatten}
(implement advance_(back_)_by on more iterators #87091) - Change advance(_back)_by to return the remainder instead of the number of processed elements (Change advance(_back)_by to return the remainder instead of the number of processed elements #92284)
Activity
timvermeulen commentedon Oct 1, 2020
I’ve already written implementations of these methods for most iterators in
core::iter
, I’ll submit PRs for them once I have enough tests.Auto merge of rust-lang#77594 - timvermeulen:chain_advance_by, r=scot…
GuillaumeGomez commentedon Oct 13, 2020
The fact that
advance_by
returns aResult
isn't great in my opinion. In case it moves after the end of the iterator, it's not an error, just that it ran after the end. It would make more sense to returnSome(how_much_after_the_end)
instead ofErr(how_much_after_the_end)
.m-ou-se commentedon Oct 23, 2020
That means that every exising Iterator implementation out there that already has an efficient nth() will now automatically get a default slow advance_by() added, implemented as a next() loop, right? I don't think it can be expected that all these implementations will be updated overnight the moment this hits stable, or even get updated at all. Generic code will then have to make a choice between 1) being efficient/simple with std's Iterators but potentially very slow with other Iterators (
advance_by
) and 2) being efficient with all iterators but with a slightly more confusing function (nth
). :(I like
advance_by
, but I feel like this problem is not something to ignore.m-ou-se commentedon Oct 23, 2020
Returning an
Option
also isn't great.None
in the context of anIterator
usually means the end is reached, so usingNone
here for not reaching the end can also get confusing.I'm wondering about the downside of having to return the 'missing length' from advance_by. I can imagine there are data structures that do not store their length, but do have an upper bound on their length. (E.g. a fixed size buffer containing a C-style 0-terminated string.) Those can just return
None
directly fromnth(n)
ifn
is>=
the maximum length.advance_by(n)
would still require counting the elements to return the rightErr(i)
, even if the caller is probably just going to use.is_ok()
. Having these two similar functions (nth
andadvance_by
) where you can't know in general which one is the more efficient one would be a shame.What are the advantages of returning
Err(missing_length)
as opposed toNone
orfalse
?kevincox commentedon Oct 23, 2020
Since
advance_by
returns the number of items you can't implement in terms of.nth()
which was the previous recommended way to skip elements. (one exception is that you could usenth
for cases wheresize_hint().1 > 0
but this doesn't solve the recursion problem discussed below) I think that returning the number of elements is a useful feature so I wouldn't want to avoid it. This is also something that.nth()
lacks today.However even if it didn't return the length it isn't clear how the default would be implemented. You can't have the defaults of
advance_by
andnth
refer to each other because then if neither was implemented you would have infinite recursion. And if we implementadvance_by(n)
asif n > 0 { self.nth(n-1) }
then for maximum performance you would need to implement bothadvance_by
andnth
. I think it is best to avoid this legacy.So while I admit that it is unfortunate that switching callers from
nth
toadvance_by
can bypass an optimizednth
until the iterator implementation is updated I think that this is the best solution available.timvermeulen commentedon Oct 23, 2020
@m-ou-se
You're absolutely right, which is why we're not going to change any uses of
nth
in libcore toadvance_by
(on potentially user-defined iterator types), even if it would clean up the code. It's an unfortunate side-effect of the fact thatnth
was here first.The main problem that
advance_by
solves is that it composes well in ways thatnth
doesn't for iterators likeChain
. For example, in order to efficiently compute(0..10).chain(10..20).chain(20..30).nth(25)
, it's crucial that the first inner iterator ofChain
doesn't just indicate that it doesn't have ann
th element (or that it isn't able to advance byn
), but that it also somehow indicates how much progress was made in order to know which value ofn
to pass to the second iterator.nth
isn't able to solve this problem on its own.I don't really have a satisfying answer to this... There are indeed iterators out there for which
nth
can be faster thanadvance_by
for certain inputs, although I do believe that these are relatively rare. The best way to handle this might just be to only useadvance_by
if either the iterator type is known to have an equally efficientadvance_by
implementation, or you need theErr(n)
value for something. An example of that is how the current implementation ofChain::nth
callsadvance_by
on the first iterator (because it needs the error value) andnth
on the second.32 remaining items
the8472 commentedon Oct 13, 2021
It would be mostly useful for minor optimization to skip a followup operation, such as the
next()
nth
or to drop the front iterator in Flatten. We can obviously already do that in theErr
case because we know the iterator is exhausted. But in theOk
case we don't know that.(usize, bool)
could convey those pieces orthogonally.Granted, the value of that distinction is minor, all it costs to notice the exhaustion is another call to
next
oradvance_by
.Many of the implementations need to do that anyway.
https://github.com/rust-lang/rust/pull/87091/files#diff-c0d520d60171cd367fdbe3ad1387b13cd2be83597f623d4209f8ca4fc1394f56R398
the8472 commentedon Oct 15, 2021
#89916 removes the inconsistency from the docs and updates the implementations to always return
Ok(())
in that case.iter::Cloned::advance_{back}_by()
#90209the8472 commentedon Dec 26, 2021
For discussion: #92284 which changes the return type to
usize
. The change does simplify a few things.kkharji commentedon Apr 28, 2022
What is blocking this feature? advance_by is super useful.
jonas-schievink commentedon Jun 21, 2022
This method is one of only 4 in the vtable of
dyn Iterator
(alongsidenext
,size_hint
andnth
). Are you sure that incurring this codegen bloat for every Rust program that usesdyn Iterator
is worth it, or should there be awhere Self: Sized
bound on this method?Rollup merge of rust-lang#99703 - dtolnay:tokenstreamsizehint, r=petr…
Rollup merge of #99703 - dtolnay:tokenstreamsizehint, r=petrochenkov
the8472 commentedon Mar 13, 2023
#92284 has been updated if anyone wants to take a look, it now returns a
Result<(), NonZeroUsize>
where the error case represents the amount of items that couldn't be drained, rather than the number that could.fix: remove unneeded nightly feature flag
fix: remove unneeded nightly feature flag
slice::IterMut
type yoshuawuyts/beton#1Replace custom `advance_by` with `Iterator::nth`