问题描述
我正在实现一些递归代码,其中在调用堆栈中更深的函数实例可能需要引用先前帧中的数据。但是,我只能对这些数据进行非Mut访问,因此我将这些数据作为引用接收。因此,我需要将对这些数据的引用保留在可以从更深的实例访问的堆栈数据结构中。
说明:
// I would like to implement this RefStack class properly,without per-item memory allocations
struct RefStack<T: ?Sized> {
content: Vec<&T>,}
impl<T: ?Sized> RefStack<T> {
fn new() -> Self { Self{ content: Vec::new() } }
fn get(&self,index: usize) -> &T { self.content[index] }
fn len(&self) -> usize { self.content.len() }
fn with_element<F: FnOnce(&mut Self)>(&mut self,el: &T,f: F) {
self.content.push(el);
f(self);
self.content.pop();
}
}
// This is just an example demonstrating how I would need to use the RefStack class
fn do_recursion(n: usize,node: &LinkedListNode,st: &mut RefStack<str>) {
// get references to one or more items in the stack
// the references should be allowed to live until the end of this function,but shouldn't prevent me from calling with_element() later
let tmp: &str = st.get(rng.gen_range(0,st.len()));
// do stuff with those references (println is just an example)
println!("Item: {}",tmp);
// recurse deeper if necessary
if n > 0 {
let (head,tail): (_,&LinkedListNode) = node.get_parts();
manager.get_str(head,|s: &str| // the actual string is a local variable somewhere in the implementation details of get_str()
st.with_element(s,|st| do_recursion(n - 1,tail,st))
);
}
// do more stuff with those references (println is just an example)
println!("Item: {}",tmp);
}
fn main() {
do_recursion(100,list /* gotten from somewhere else */,&mut RefStack::new());
}
在上面的示例中,我担心如何在没有每个项目内存分配的情况下实现RefStack
。 Vec
偶尔进行的分配是可以接受的-分配很少,而且介于两者之间。 LinkedListNode
只是一个示例-实际上,它是一些复杂的图形数据结构,但同样适用-我仅对其具有非mut引用,而给manager.get_str()
的闭包仅提供了一个非Mut str
。请注意,传递给闭包的非mut str
只能在get_str()
实现中构造,因此我们不能假定所有&str
都具有相同的生存期。
我相当肯定RefStack
不能在不将str
复制到拥有的String
的情况下在安全Rust中实现,所以我的问题是如何在不安全的锈。感觉我也许可以得到这样的解决方案:
- 不安全仅限于实施
RefStack
- 由
st.get()
返回的引用的生存期至少应与do_recursion
函数的当前实例一样长(特别是,它应该能够保留对st.with_element()
的调用,并且这在逻辑上是安全的,因为&T
返回的st.get()
并不指向RefStack
拥有的任何内存)
如何在(不安全的)Rust中实现这样的结构?
感觉到我可以将元素引用转换为指针并将其存储为指针,但是在将它们转换回引用时,在上面第二个要点中表达要求仍然会遇到困难。还是有更好的方法(或者有可能在安全的Rust中实现这种结构,或者已经在某个库中的某个地方实现了这种结构)?
解决方法
免责声明:这个答案最初使用的是特质,这是一场噩梦;弗朗西斯·加涅(Francis Gagne)正确地指出,在尾巴上使用Option
是更好的选择,因此答案得到了大大简化。
根据使用情况的结构,在使用RefStack
中的堆栈之后,使用堆栈框架,您可以简单地将元素放在堆栈框架上,然后从中构建堆栈。
这种方法的主要优点是它是完全安全的。您可以查看whole code here,也可以按照下面的介绍进行介绍。
关键是想法是建立一个所谓的缺点列表。
#[derive(Debug)]
struct Stack<'a,T> {
element: &'a T,tail: Option<&'a Stack<'a,T>>,}
impl<'a,T> Stack<'a,T> {
fn new(element: &'a T) -> Self { Stack { element,tail: None } }
fn top(&self) -> &T { self.element }
fn get(&self,index: usize) -> Option<&T> {
if index == 0 {
Some(self.element)
} else {
self.tail.and_then(|tail| tail.get(index - 1))
}
}
fn tail(&self) -> Option<&'a Stack<'a,T>> { self.tail }
fn push<'b>(&'b self,element: &'b T) -> Stack<'b,T> { Stack { element,tail: Some(self) } }
}
一个简单的用法示例是:
fn immediate() {
let (a,b,c) = (0,1,2);
let root = Stack::new(&a);
let middle = root.push(&b);
let top = middle.push(&c);
println!("{:?}",top);
}
仅打印堆栈,产生:
Stack { element: 2,tail: Some(Stack { element: 1,tail: Some(Stack { element: 0,tail: None }) }) }
还有更详尽的递归版本:
fn recursive(n: usize) {
fn inner(n: usize,stack: &Stack<'_,i32>) {
if n == 0 {
print!("{:?}",stack);
return;
}
let element = n as i32;
let stacked = stack.push(&element);
inner(n - 1,&stacked);
}
if n == 0 {
println!("()");
return;
}
let element = n as i32;
let root = Stack::new(&element);
inner(n - 1,&root);
}
哪些印刷品:
Stack { element: 1,tail: Some(Stack { element: 2,tail: Some(Stack { element: 3,tail: None }) }) }
一个缺点是get
的性能可能不太好;它具有线性复杂度。另一方面,按高速缓存粘在堆栈帧上非常不错。如果您主要访问前几个元素,那么我希望它足够好。
我认为存储原始指针是必经之路。您需要一个PhantomData
来存储生存期并获得适当的协方差:
use std::marker::PhantomData;
struct RefStack<'a,T: ?Sized> {
content: Vec<*const T>,_pd: PhantomData<&'a T>,T: ?Sized> RefStack<'a,T> {
fn new() -> Self {
RefStack {
content: Vec::new(),_pd: PhantomData
}
}
fn get(&self,index: usize) -> &'a T {
unsafe { &*self.content[index] }
}
fn len(&self) -> usize {
self.content.len()
}
fn with_element<'t,F: FnOnce(&mut RefStack<'t,T>)>(&mut self,el: &'t T,f: F)
where 'a: 't,{
self.content.push(el);
let mut tmp = RefStack {
content: std::mem::take(&mut self.content),_pd: PhantomData,};
f(&mut tmp);
self.content = tmp.content;
self.content.pop();
}
}
唯一的unsafe
代码是将指针转换回引用。
棘手的部分是正确设置with_element
。我认为were 'a: 't
是隐式的,因为整个impl
都依赖它(但比后悔更安全)。
最后一个问题是如何将RefStack<'a,T>
转换为RefStack<'t,T>
。我很确定我可以std::transmute
。但这将需要额外注意unsafe
,并且创建一个新的临时堆栈非常简单。
关于't
的生存期
您可能会认为实际上不需要这个't
的生命周期,但是不添加它可能会引起一些不完善的情况,因为回调函数可能会调用get()
并获得生命周期为'a
的值实际上比插入值长。
例如,此代码不应编译。使用't
,它可以正确地失败,但是如果没有它,它将编译并导致未定义的行为:
fn breaking<'a,'s,'x>(st: &'s mut RefStack<'a,i32>,v: &'x mut Vec<&'a i32>) {
v.push(st.get(0));
}
fn main() {
let mut st = RefStack::<i32>::new();
let mut y = Vec::new();
{
let i = 42;
st.with_element(&i,|stack| breaking(stack,&mut y));
}
println!("{:?}",y);
}
关于panic!
。
在做这些不安全的事情时,尤其是当您在调用用户代码时,就像我们在with_element
中所做的那样,我们必须考虑如果发生恐慌会发生什么。在OP代码中,最后一个对象 not 不会弹出,并且当堆栈展开后,任何drop
实现都可以看到悬挂的引用。我的代码可以解决紧急情况,因为如果f(&mut tmp);
悬空的引用在tmp
为空时死在本地临时self.content
中。
免责声明:不同的答案;权衡取舍。
与我的其他答案相比,该答案提供了一种解决方案:
- 不安全:它是封装的,但微妙。
- 易于使用。
- 更简单的代码,可能更快。
想法是 still 使用堆栈来绑定引用的生存期,但将所有生存期存储在单个Vec
中以进行O(1)随机访问。因此,我们在堆栈上构建堆栈,但没有将引用本身存储在堆栈中。好吗?
完整代码is available here。
堆栈本身很容易定义:
struct StackRoot<T: ?Sized>(Vec<*const T>);
struct Stack<'a,T: ?Sized>{
len: usize,stack: &'a mut Vec<*const T>,}
impl<T: ?Sized> StackRoot<T> {
fn new() -> Self { Self(vec!()) }
fn stack(&mut self) -> Stack<'_,T> { Stack { len: 0,stack: &mut self.0 } }
}
Stack
的实现比较棘手,就像涉及unsafe
时一样:
impl<'a,T: ?Sized> Stack<'a,T> {
fn len(&self) -> usize { self.len }
fn get(&self,index: usize) -> Option<&'a T> {
if index < self.len {
// Safety:
// - Index is bounds as per above branch.
// - Lifetime of reference is guaranteed to be at least 'a (see push).
Some(unsafe { &**self.stack.get_unchecked(index) })
} else {
None
}
}
fn push<'b>(&'b mut self,T>
where
'a: 'b
{
// Stacks could have been built and forgotten,resulting in `self.stack`
// containing references to further elements,so that the newly pushed
// element would not be at index `self.len`,as expected.
//
// Note that on top of being functionally important,it's also a safety
// requirement: `self` should never be able to access elements that are
// not guaranteed to have a lifetime longer than `'a`.
self.stack.truncate(self.len);
self.stack.push(element as *const _);
Stack { len: self.len + 1,stack: &mut *self.stack }
}
}
impl<'a,T: ?Sized> Drop for Stack<'a,T> {
fn drop(&mut self) {
self.stack.truncate(self.len);
}
}
在这里记下unsafe
;不变的是'a
参数始终严格 ,以确保元素到堆栈中的生存期到目前为止。
通过拒绝访问其他成员推送的元素,我们保证返回的引用的有效期是有效的。
它确实需要do_recursion
的通用定义,但是在代码生成时会删除通用生命周期参数,因此不涉及代码膨胀:
fn do_recursion<'a,'b>(nodes: &[&'a str],stack: &mut Stack<'b,str>)
where
'a: 'b
{
let tmp: &str = stack.get(stack.len() - 1).expect("Not empty");
println!("{:?}",tmp);
if let [head,tail @ ..] = nodes {
let mut new = stack.push(head);
do_recursion(tail,&mut new);
}
}
一个简单的main
来炫耀它:
fn main() {
let nodes = ["Hello",","World","!"];
let mut root = StackRoot::new();
let mut stack = root.stack();
let mut stack = stack.push(nodes[0]);
do_recursion(&nodes[1..],&mut stack);
}
结果:
,"Hello" "," "World" "!"
基于rodrigo's answer,我实现了这个稍微简单的版本:
struct RefStack<'a,T: ?Sized + 'static> {
content: Vec<&'a T>,T: ?Sized + 'static> RefStack<'a,}
}
fn get(&self,index: usize) -> &'a T {
self.content[index]
}
fn len(&self) -> usize {
self.content.len()
}
fn with_element<'t,F: >(&mut self,f: F)
where
F: FnOnce(&mut RefStack<'t,T>),'a: 't,{
let mut st = RefStack {
content: std::mem::take(&mut self.content),};
st.content.push(el);
f(&mut st);
st.content.pop();
self.content = unsafe { std::mem::transmute(st.content) };
}
}
与rodrigo解决方案的唯一区别是,向量表示为引用的向量而不是指针的向量,因此我们不需要PhantomData
和不安全的代码即可访问元素。
将新元素推入with_element()
中的堆栈时,我们要求它的生存期要短于绑定了a': t'
的现有元素。然后,我们创建一个具有较短生命周期的新堆栈,这在安全代码中是可行的,因为我们知道向量中引用所指向的数据指向的生命周期更长'a
甚至是生命。然后,我们以安全代码再次将具有生存期't
的新元素推入新向量,并且只有在再次移除该元素之后,才将向量移回其原始位置。这需要不安全的代码,因为这次我们将向量中的引用的生存期从't
扩展到'a
。我们知道这是安全的,因为向量返回到其原始状态,但是编译器不知道这一点。
我觉得这个版本比Rodrigo几乎相同的版本更好地表达了意图。向量的类型始终是“正确的”,因为它描述了元素实际上是引用,而不是原始指针,并且总是为向量分配正确的生存期。而且,当延长向量中引用的生存期时,我们会在发生潜在不安全情况的地方完全使用不安全代码。