Struct orx_imp_vec::ImpVec
source · pub struct ImpVec<T, P = SplitVec<T>>where
P: PinnedVec<T>,{ /* private fields */ }Expand description
A data structure to enable conveniently building performant self referential collections such as trees, graphs and linked lists.
ImpVec 👿 is a wrapper around a PinnedVec which guarantees that memory location of its elements do not change unless elements are removed from the vector. In addition to dereferencing the PinnedVec methods, ImpVec adds the following methods which are specialized for defining and building self referential collections using plain & references:
fn set_next(&mut self, element: &T, next: Option<&'a T>)
fn set_prev(&mut self, element: &T, prev: Option<&'a T>)
fn replace_at(&mut self, at: usize, new_value: T) -> Option<T>
unsafe fn push_get_ref(&self, value: T) -> &T
unsafe fn move_get_ref(&mut self, source_idx: usize, destination_idx: usize, fill_source_with: T) -> Option<(&T, &T)>Implementations§
source§impl<T, P: PinnedVec<T>> ImpVec<T, P>
impl<T, P: PinnedVec<T>> ImpVec<T, P>
sourcepub fn into_pinned(self) -> P
pub fn into_pinned(self) -> P
Converts the ImpVec into underlying PinnedVec.
This operation corresponds to moving the pinned vec out of a ref cell; and hence, is not expensive. Likewise, the pinned vec can then be converted back into an imp vec by wrapping it up by a ref cell under the hood.
Converting an ImpVec to PinnedVec is useful for the following reasons:
- It reduces one level of abstraction by getting rid of the ref cell, and hence, achieving SplitVec or FixedVec (same as std vec) performance depending on the type of the pinned vec.
- It means that immutable pushes are not allowed anymore.
- In a vector holding inter-element references, this means that such references will not be added or removed.
- In other words, it safely ensures that the building of the structure is completed.
Examples
You may see and example back-and-forth conversions between a PinnedVec.
Complete type annotations are not needed byt added to make the conversions explicitly visible.
use orx_imp_vec::prelude::*;
let mut imp: ImpVec<char, SplitVec<char, Doubling>> = SplitVec::with_doubling_growth().into();
for _ in 0..3 {
imp.push('s');
}
assert_eq!(imp, vec!['s', 's', 's']);
let mut split: SplitVec<char, Doubling> = imp.into_pinned();
assert_eq!(split, vec!['s', 's', 's']);
let mut imp_again: ImpVec<_, _> = split.into();
imp_again.push('s');
assert_eq!(imp_again, vec!['s', 's', 's', 's']);
let split_back = imp_again.into_pinned();
assert_eq!(split_back, vec!['s', 's', 's', 's']);source§impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
sourcepub fn replace_at(&mut self, at: usize, new_value: T) -> Option<T>
pub fn replace_at(&mut self, at: usize, new_value: T) -> Option<T>
Replaces the data at the at-th position of the vector with the new_value;
and returns the old data at this position.
Does nothing and returns None if at is out of bounds.
Safety
This method is safe due to the following:
- acts only if
atis within the storage owned by the vector. - all references to the element at the
at-th position are still valid since the length of the vector does not change. - the underlying data at this position change; however, this is done with a
&mut selfreference.
sourcepub unsafe fn push_get_ref<'b>(&'b mut self, value: T) -> &'a Twhere
P: 'a,
'a: 'b,
pub unsafe fn push_get_ref<'b>(&'b mut self, value: T) -> &'a Twhere
P: 'a,
'a: 'b,
Appends an element to the back of a collection and returns a reference to it.
Unlike std::vec::Vec or orx_split_vec::SplitVec;
push operation for ImpVec does not require a mutable reference (see Safety section).
This is a crucial method for building self-referential-collections which is the underlying motivation of an ImpVec.
See the Motivation section for details.
Examples
use orx_imp_vec::prelude::{ImpVec, PinnedVec};
let mut vec: ImpVec<_> = ImpVec::new();
let ref1 = unsafe { vec.push_get_ref(1) };
let ref_elem_addr = ref1 as *const i32;
vec.push(2);
vec.push(3);
let ref4 = unsafe { vec.push_get_ref(4) };
// capacity is expaneded here from 4 to 8; however, in chunks;
// therefore, data is not moved around and the references remain valid.
let ref5 = unsafe { vec.push_get_ref(5) };
assert_eq!(ref1, &1);
assert_eq!(ref4, &4);
assert_eq!(ref5, &5);
assert_eq!(vec, [1, 2, 3, 4, 5]);
let ref_elem_addr_after_growth = &vec[0] as *const i32;
assert_eq!(ref_elem_addr, ref_elem_addr_after_growth);As you may see below, any mutable method that can possibly invalidate the references are not allowed.
use orx_imp_vec::prelude::*;
let mut vec: ImpVec<_, _> = SplitVec::with_linear_growth(10).into(); // mut required for the `insert`
let ref1 = unsafe { vec.push_get_ref(1) };
vec.push(2);
vec.push(3);
assert_eq!(ref1, &1);
assert_eq!(vec, [1, 2, 3]);
vec.insert(0, 42);
assert_eq!(vec, [42, 1, 2, 3]);
// below line does not compile as the 'insert' call breaks reference 'ref1'
// let value1 = *ref1;Safety
This method is unsafe due to the broken lifetime relation of the returned reference from this vector. In brief, the returned reference can outlive this vector which is undefined behavior (UB). The following example demonstrates that this UB can be experienced.
use orx_imp_vec::prelude::*;
let mut imp: ImpVec<_> = ImpVec::new();
let first = unsafe { imp.push_get_ref('a') };
assert_eq!(&'a', &imp[0]);
assert_eq!(&'a', first);
drop(imp);
// assert_eq!(&'a', &imp[0]); // this should NOT compile, and correctly does NOT compile
// the following should also NOT compile; however, it does!
assert_eq!(&'a', first);Due to the demonstrated problem, push_get_ref method is unsafe and should be used carefully.
In particular, caller we must take the responsibility to make sure that the returned reference does not outlive the vector. The wrapper structures or methods can safely be used once this guarantee is provided.
Motivation
The reason this method exists is to narrow down the unsafe code and complexity.
To be precise, the entire complexity of building self-referential-collections can be narrowed to the limited usage of
two main unsafe methods push_get_ref and crate::ImpVec::move_get_ref.
Consider for instance two doubly-linked list implementations std::collections::LinkedList and orx_linked_list::LinkedList.
std::collections::LinkedList is a relatively low level implementation with heavy use of raw pointers.
The file https://doc.rust-lang.org/src/alloc/collections/linked_list.rs.html
contains unsafe keyword used more than 60 times.
The unsafe code blocks contain reads/writes to memory through raw pointers which is dangerous and allows much more unsafety than required for defining a linked list.
A higher level implementation can be found here https://crates.io/crates/orx-linked-list
which is built on an underlying ImpVec storage.
Complete repository contains the unsafe keyword seven times corresponding to repeated usage of only three methods:
ImpVec::push_get_refImpVec::move_get_refImpVec::unsafe_truncatewhich actually is a deref method fromPinnedVec::unsafe_truncate
In brief, a full-fetched doubly-linked-list can be built in rust:
- with thin references
- without smart pointers
- without any raw pointers
- by using only three unsafe methods which are specialized for building self referential collections
Furthermore, it performs significantly faster proving the performance benefit by cache locality which can be attained by
putting nodes close to each other within an ImpVec.
sourcepub unsafe fn move_get_ref<'b>(
&mut self,
source_idx: usize,
destination_idx: usize,
fill_source_with: T
) -> Option<(&'b T, &'b T)>where
'a: 'b,
pub unsafe fn move_get_ref<'b>(
&mut self,
source_idx: usize,
destination_idx: usize,
fill_source_with: T
) -> Option<(&'b T, &'b T)>where
'a: 'b,
Performs the following:
- copies the data at
source_idxonto the element atdestination_idx; - replaces the data at the
source_idxwith thefill_source_with; - returns references to the new values of the elements at (source_idx, destination_idx).
Does nothing and returns None if any of source_idx and destination_idx is out of bounds.
Note that after the move operation
- value at the
destination_idxwill be equal to the prior value at thesource_idx, and - value at the
source_idxwill be equal tofill_source_with.
Example
use orx_imp_vec::*;
let mut imp: ImpVec<_> = ['a', 'b', 'c'].into_iter().collect();
assert_eq!(&['a', 'b', 'c'], &imp);
let (ref0, ref1) = unsafe { imp.move_get_ref(0, 1, 'x') }.unwrap();
assert_eq!(&imp[0], &'x');
assert_eq!(&imp[1], &'a');
assert_eq!(ref0, &'x');
assert_eq!(ref1, &'a');Safety
This method is safe due to the following:
- acts only if
source_idxanddestination_idxare within the storage owned by the vector. - all references to the element at the
source_idx-th position anddestination_idx-th position are still valid since the length of the vector does not change. - the underlying data at these positions change; however, this is done with a
&mut selfreference.
On the other hand, this method is unsafe due to the broken lifetime relation of the returned references from this vector. In brief, the returned references can outlive this vector which is undefined behavior (UB). The following example demonstrates that this UB can be experienced.
use orx_imp_vec::*;
let mut imp: ImpVec<_> = ['a', 'b', 'c'].into_iter().collect();
assert_eq!(&['a', 'b', 'c'], &imp);
let (ref0, ref1) = unsafe { imp.move_get_ref(0, 1, 'x') }.unwrap();
assert_eq!(&imp[0], &'x');
assert_eq!(&imp[1], &'a');
assert_eq!(ref0, &'x');
assert_eq!(ref1, &'a');
drop(imp);
// following two lines should NOT compile, and correctly do NOT compile
// assert_eq!(&imp[0], &'x');
// assert_eq!(&imp[1], &'a');
// the following lines should also NOT compile; however, they do!
assert_eq!(ref0, &'x');
assert_eq!(ref1, &'a');Due to the demonstrated problem, move_get_ref method is unsafe and should be used carefully.
In particular, caller we must take the responsibility to make sure that the returned references does not outlive the vector. The wrapper structures or methods can safely be used once this guarantee is provided.
Motivation
The reason this method exists is to narrow down the unsafe code and complexity.
To be precise, the entire complexity of building self-referential-collections can be narrowed to the limited usage of
two main unsafe methods move_get_ref and crate::ImpVec::push_get_ref.
Consider for instance two doubly-linked list implementations std::collections::LinkedList and orx_linked_list::LinkedList.
std::collections::LinkedList is a relatively low level implementation with heavy use of raw pointers.
The file https://doc.rust-lang.org/src/alloc/collections/linked_list.rs.html
contains unsafe keyword used more than 60 times.
The unsafe code blocks contain reads/writes to memory through raw pointers which is dangerous and allows much more unsafety than required for defining a linked list.
A higher level implementation can be found here https://crates.io/crates/orx-linked-list
which is built on an underlying ImpVec storage.
Complete repository contains the unsafe keyword seven times corresponding to repeated usage of only three methods:
ImpVec::push_get_refImpVec::move_get_refImpVec::unsafe_truncatewhich actually is a deref method fromPinnedVec::unsafe_truncate
In brief, a full-fetched doubly-linked-list can be built in rust:
- with thin references
- without smart pointers
- without any raw pointers
- by using only three unsafe methods which are specialized for building self referential collections
Furthermore, it performs significantly faster proving the performance benefit by cache locality which can be attained by
putting nodes close to each other within an ImpVec.
source§impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
T: SelfRefNext<'a> + 'a,
impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
T: SelfRefNext<'a> + 'a,
sourcepub fn set_next(&mut self, element: &T, next: Option<&'a T>)
pub fn set_next(&mut self, element: &T, next: Option<&'a T>)
Using interior mutability peforms the following:
element.set_next(next)
so that element will point to the next element after the operation.
Panics
Panics:
- if
elementdoes not belong to this vector, or - if
nextis of Some variant and underlying reference does not belong to this vector.
Safety
Due to the guards defined in the Panics section, element and next (if some) belong to the same vector living together and sharing the same lifetime.
Example
This is one of the specialized methods for building self-referential-collections when the elements implement orx_pinned_vec::SelfRefNext.
It is not trivial to provide a partial and trivial example.
However, the usefulness of this method can be demonstrated by its usage within the orx_linked_list::LinkedList implementation.
The crate defines a double-linked-list Node with optional next and prev references.
Node implements orx_pinned_vec::SelfRefNext and orx_pinned_vec::SelfRefPrev; therefore, we can use ImpVec::set_next method.
The following code block is the push_back method implementation of the LinkedList:
pub fn push_back(&mut self, value: T) {
match self.back_node() {
None => self.push_first_node(value),
Some(old_back) => {
let node = Node::active(value, Some(old_back), None);
let back = unsafe { self.vec.push_get_ref(node) };
self.vec.set_next(old_back, Some(back));
self.slice = LinkedListSlice::new(self.len() + 1, self.front_node(), Some(back));
}
}
}In the trivial case when there is no back_node (the list is empty), we simply push the value as the first node.
Otherwise:
- We get a reference to the back node, we’ll now call it
old_backsince it will no longer be the back of the list. - We create a new
nodefor the newvalue:- previous node of
nodeis set toSome(old_back), - next node of
nodeisNonesince it is the new back of the list.
- previous node of
- We use the
push_get_refmethod to pushnodeto the storage vector and get a reference to it.- SAFETY: we must make sure that the reference
backdoes not outliveself; which is satisfied here.
- SAFETY: we must make sure that the reference
- We tell our
self.vecwhich is anImpVecto set the next ofold_backtoSome(back)to link the old back to the new back. - Then, we set that our list now is one element longer, with the old
self.front_node()and new back node which isSome(back).
source§impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
T: SelfRefPrev<'a> + 'a,
impl<'a, T, P> ImpVec<T, P>where
P: PinnedVec<T> + 'a,
T: SelfRefPrev<'a> + 'a,
sourcepub fn set_prev(&mut self, element: &T, prev: Option<&'a T>)
pub fn set_prev(&mut self, element: &T, prev: Option<&'a T>)
Using interior mutability peforms the following:
element.set_prev(prev)
so that element will point to the prev element after the operation.
Panics
Panics:
- if
elementdoes not belong to this vector, or - if
previs of Some variant and underlying reference does not belong to this vector.
Safety
Due to the guards defined in the Panics section, element and prev (if some) belong to the same vector living together and sharing the same lifetime.
Example
This is one of the specialized methods for building self-referential-collections when the elements implement orx_pinned_vec::SelfRefNext.
It is not trivial to provide a partial and trivial example.
However, the usefulness of this method can be demonstrated by its usage within the orx_linked_list::LinkedList implementation.
The crate defines a double-linked-list Node with optional next and prev references.
Node implements orx_pinned_vec::SelfRefNext and orx_pinned_vec::SelfRefPrev; therefore, we can use ImpVec::set_next method.
The following code block is the push_front method implementation of the LinkedList:
pub fn push_front(&mut self, value: T) {
match self.front_node() {
None => self.push_first_node(value),
Some(old_front) => {
let node = Node::active(value, None, Some(old_front));
let front = unsafe { self.vec.push_get_ref(node) };
self.vec.set_prev(old_front, Some(front));
self.slice = LinkedListSlice::new(self.len() + 1, Some(front), self.back_node());
}
}
}In the trivial case when there is no front_node (the list is empty), we simply push the value as the first node.
Otherwise:
- We get a reference to the front node, we’ll now call it
old_frontsince it will no longer be the front of the list. - We create a new
nodefor the newvalue:- next node of
nodeis set toSome(old_front), - previous node of
nodeisNonesince it is the new front of the list.
- next node of
- We use the
push_get_refmethod to pushnodeto the storage vector and get a reference to it.- SAFETY: we must make sure that the reference
frontdoes not outliveself, which is satisfied here.
- SAFETY: we must make sure that the reference
- We tell our
self.vecwhich is anImpVecto set the prev ofold_fronttoSome(front)to link the old front node to the new front ndoe. - Then, we set that our list now is one element longer, with the old
self.back_node()and new front node which isSome(front).