-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tracking issue for LinkedList cursors #58533
Comments
While the RFC doesn't explicitly show how these APIs should be documented. I think that for operations like |
one thing that I think would be a useful addition is a |
Does the Walker "borrow" the list, preventing removals? If so, why does one need to combine it with borrowing the list? If not, how does the Walker handle the case, in which the position it refers to has been deleted? |
no, the walker doesn't borrow the list. the list would panic or return None when passed a walker to a deleted item. to prevent needing the extra space in a list to track deleted nodes to prevent dangling pointers, we could add a WalkableList struct that borrows the list and keeps deleted nodes from being freed until the WalkableList is dropped or changes Walkers pointing to the deleted item to point to nothing. all access of the list would go through WalkableList. WalkableList would keep track of the Walkers and on drop would panic if there are any walkers left. we may want to make a generic Walkable trait to allow walking types other than linked lists. Something like this: struct WalkableList<'a, T> {
list: &'a mut LinkedList<T>,
deletedNodes: Option<NonNull<Node<T>>, // element already dropped when in this list
walkerTracker: Rc<()>,
}
#[derive(Clone)]
struct Walker<T> {
node: Option<NonNull<Node<T>>>,
walkerTracker: Rc<()>,
}
struct Node<T> {
next: Option<NonNull<Node<T>>>,
prev: Option<NonNull<Node<T>>>,
element: ManuallyDrop<T>,
}
impl<T> Drop for WalkableList<'_, T> {
fn drop(&mut self) {
assert!(self.walkerTracker.get_mut().is_some(), "Walkers left");
while let Some(node) = self.deletedNodes {
let node = Box::from_raw(node.as_ptr());
self.deletedNodes = node.next;
// don't drop node.element; already dropped
}
}
}
impl<T> WalkableList<'_, T> {
pub fn begin(&self) -> Walker<T> {
Walker { node: self.list.head, walkerTracker: self.walkerTracker.clone(), }
}
pub fn remove(&mut self, w: &Walker<T>) -> Option<T> {
assert!(Rc::ptr_eq(&self.walkerTracker, &w.walkerTracker);
let node = w.node?;
unsafe {
if node.as_ref().prev == Some(node) {
// node is deleted
return None;
}
self.list.unlink_node(node);
let retval = ManuallyDrop::take(&mut node.as_mut().element);
node.prev = Some(node); // mark as deleted
node.next = self.deletedNodes;
self.deletedNodes = node;
Some(retval)
}
}
pub fn get(&self, w: &Walker<T>) -> Option<&T> {
// ...
}
pub fn get_mut(&mut self, w: &Walker<T>) -> Option<&mut T> {
assert!(Rc::ptr_eq(&self.walkerTracker, &w.walkerTracker);
let node = w.node?;
unsafe {
if node.as_ref().prev == Some(node) {
// node is deleted
return None;
}
Some(&mut *(*node.as_ptr()).element)
}
}
// other members
} |
I'm working on this. Currently i'm writing unit tests. Will put up a PR soon. |
rust-lang/rfcs#2847 was open to suggest making a number of changes to this rfc. |
Implement Cursor for linked lists. (RFC 2570). cc. rust-lang#58533 cc. @Gankra r? @Amanieu
Implement Cursor for linked lists. (RFC 2570). cc. rust-lang#58533 cc. @Gankra r? @Amanieu
Implement Cursor for linked lists. (RFC 2570). cc. rust-lang#58533 cc. @Gankra r? @Amanieu
Implement Cursor for linked lists. (RFC 2570). cc. rust-lang#58533 cc. @Gankra r? @Amanieu
#68123 has been merged. |
Any reason why |
It seems to just be an oversight. Feel free to send a PR to add |
Something I needed just now was to move an element from somewhere within the list to the front. |
Maybe |
Add remove_current_as_list to LinkedList's CursorMut The `remove_current` method only returns the inner `T` and deallocates the list node. This is unnecessary for move operations, where the element is going to be linked back into this (or even a different) `LinkedList`. The `remove_current_as_list` method avoids this by returning the unlinked list node as a new single-element `LinkedList` structure. (per rust-lang#58533 (comment))
Remove `linked_list_extras` methods. Removing these in favor of the `Cursor` API in rust-lang#58533 . Closes rust-lang#27794. r? @Amanieu
Remove `linked_list_extras` methods. Removing these in favor of the `Cursor` API in rust-lang#58533 . Closes rust-lang#27794. r? @Amanieu
A cursor API for |
I've got one for |
I'd say that a |
Is there any reason why I often want to both browse and append to a |
C++ has a notion of "Bidirectional Iterators" (ref1, ref2). If I understand what it does correctly, translated to Rust it would resemble: /// An iterator able to step backwards.
pub trait BidirectionalIterator: Iterator {
/// Steps backwards in the iterator and returns the previous value.
///
/// Returns [`None`] when there is no previous item left to return.
fn prev(&mut self) -> Option<Self::Item>;
}
|
C++'s bidirectional iterators are waay more similar to cursors than to Rust's iterators. A C++ bidirectional iterator is like (as of C++17): // traits mixed-up slightly, check links for actual requirements
// https://en.cppreference.com/w/cpp/named_req/Iterator
unsafe trait CppIterator: Clone + Default {
type Item;
/// Safety: must not be past-the-end or otherwise invalid
/// Equivalent to C++'s `++iter`
unsafe fn move_to_next(&mut self);
// omitted `iter++`
/// Safety: must be past-the-end or before-begin or otherwise invalid
/// Equivalent to `*iter` and `iter.operator ->()`
unsafe fn get(&self) -> *mut Self::Item;
}
unsafe impl<T> CppIterator for *mut T {
type Item = T;
unsafe fn move_to_next(&mut self) {
*self = self.add(1);
}
unsafe fn get(&self) -> *mut Self::Item {
*self
}
}
// https://en.cppreference.com/w/cpp/named_req/InputIterator
unsafe trait InputIterator: PartialEq + CppIterator {}
unsafe impl<T> InputIterator for *mut T {}
// https://en.cppreference.com/w/cpp/named_req/ForwardIterator
unsafe trait ForwardIterator: InputIterator {}
unsafe impl<T> ForwardIterator for *mut T {}
// https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator
unsafe trait BidirectionalIterator: ForwardIterator {
/// Safety: must not be before-begin or otherwise invalid
/// Equivalent to C++'s `--iter`
unsafe fn move_to_prev(&mut self);
// omitted `iter--`
}
unsafe impl<T> BidirectionalIterator for *mut T {}
// https://en.cppreference.com/w/cpp/iterator/begin
// https://en.cppreference.com/w/cpp/iterator/end
unsafe trait BeginAndEnd {
type Iter: CppIterator;
/// return an iterator that is either past-the-end (in case of an empty container) or at the beginning
unsafe fn begin(self: *mut Self) -> Self::Iter;
/// return a past-the-end iterator
unsafe fn end(self: *mut Self) -> Self::Iter;
}
unsafe impl<T> BeginAndEnd for [T] {
type Iter = *mut T;
unsafe fn begin(self: *mut Self) -> Self::Iter {
self as *mut T
}
unsafe fn end(self: *mut Self) -> Self::Iter {
let len = (*self).len();
(self as *mut T).add(len)
}
} Example usage: fn cpp_example() {
let mut range = [1, 2, 3];
let range_ptr = &mut range[..] as *mut [_];
let mut iter = unsafe { BeginAndEnd::begin(range_ptr) };
while iter != unsafe { BeginAndEnd::end(range_ptr) } {
let v = unsafe { *iter.get() };
println!("{}", v);
unsafe { iter.move_to_next() };
}
}
fn rust_equivalent() {
for v in &mut [1, 2, 3] {
println!("{}", v);
}
} |
Any progress on this lately? One of my crates is still pegged to nightly as a consequence of this not being stable. Alternatively, would be happy to be directed to a crate that implements this that I can use on stable. |
I just noticed that the documentation for cursors say this:
This is incorrect: cursors point to an element, not to a location between two elements. |
I think i wrote this doc comment, sorry for my bad English! Basically i wanted to mean that "there's always a prior element and a next element", sorry for the confusing wording. The actual code behavior matches what the RFC says, i think. |
What work would need to be done to help move this forward? Is it a matter of process that I can't contribute to as an outsider, or what exactly is the current blocker? |
I was looking for a way to cheaply wrap a CursorMut around at the end of the list but that does not appear to be possible. The best workaround I found was to create a new cursor from the original list and replace the old one because it keeps a mutable reference to the list. Considering how various *front and *back opations are natively available on the cursor, I'd also expect a I implemented the following macro for moving n times in an arbitrary direction, but it's not portable. use std::cmp::Ordering::*;
// Note: can exit at the "ghost" element.
macro_rules! move_cursor_by {
($by:expr) => {
match ($by).cmp(&0) {
Equal => (),
Greater => {
for _ in 0..($by) {
if cursor.current().is_none() {
cursor = list.cursor_front_mut();
}
cursor.move_next();
}
}
Less => {
for _ in 0..-($by) {
if cursor.current().is_none() {
cursor = list.cursor_back_mut();
}
cursor.move_prev();
}
}
};
};
} |
I think we can divide methods on the cursor that function by delegating to the underlying list into two camps:
I think (1) needs to be handled by dedicated methods on the I think (2) could be handled in the general case by implementing Reverting to the start of the list could be done in |
Going back from a cursor to a list sounds like a good idea, especially if you want to use some traits/functions on the list itself like A function like |
You could implement it in safe rust efficiently, it would just be annoying. You can preserve cursor state by splitting and splicing the list.
|
I like the idea of converting a |
@4e554c4c and others. This article https://diziet.dreamwidth.org/13476.html says that allowing not more than one mutable cursor in a time makes linked list concept useless (see section "The Cursor concept") |
@safinaskar First of all, that article does not use the same terminology that you did. It just described this as
which is true! Linked lists are a niche datastructure. That doesn't mean that the API is useless, and it will still be faster than other vec-based APIs when you must splice several different LLs together: a linear operation for linked lists, but quadratic for vectors. |
I would suggest to remove index and add unsafe fn cursor_from_ptr to LinkedList which takes a pointer to the element of the list. The one useful property of linked list is O(1) insertions. It's completely nullified if you cannot access an element in O(1). E.g., |
I have thought the same and made a PR: #127189 |
hi guys, quick question, when do you think this api will be stabilized? |
@jmelo11 when someone who cares about this in my eyes pretty useless collection does the work of looking at the new API surface, writing down the reasoning behind everything, making sure everything makes sense and then proposing stabilization for it. that's a bunch of work though, and it seems like no one has cared enough to do it, which i understand. edit: accidentally closed this |
Is there a reason this couldn't be generalized for other non-Ord data types such as Vec, VecDeque, and arrays? I understand there is a lack of interest in a LinkedList-specific implementation. |
the point of this API proposal is that splitting and indexing are constant time about a certain element for linked lists. This isn't too useful for vecs where there's no need to traverse the container, and where splitting is a linear time operation cursors could be useful for trees (and other linked structures), but I haven't looked into how it would be useful for B-trees |
There is an unstable Cursor API for It's extremely useful when you want to mutate or remove multiple adjacent entries in a map. |
This is a tracking issue for the RFC "Linked list cursors" (rust-lang/rfcs#2570).
Steps:
Unresolved questions:
The text was updated successfully, but these errors were encountered: