rust Reborrow and Borrow

五月 13, 2024 [C/C++] #Rust #obsidian #编译链接

rust Reborrow and Borrow

考虑函数调用f(x),其中f: fn (&mut i32) -> i32, x: &mut i32,按照通常的逻辑,x将会被move到f的作用域中,最后在f返回时被丢弃,这样一来将无法第二次调用f(x)。然而实际上,可以无限次调用f(x),而仍保持x有效。

这是由于编译器会自动将f(x)转变为f(&mut *x),而&mut *x是所谓的reborrow操作,将会对mutable referencex再次borrow,获得一个新的mutable reference,而原本的x暂时被shadow,处于inactive状态,直到f返回时,新的mutable reference被销毁,x重新回到active状态。

对于generic函数,则不一定会进行reborrow操作,设v: &mut i32:

所谓的reborrow实际上本质上仍是borrow,也就是说Rust并没有对&*x或&mut *x引入特殊的语义。试考虑&mut *x,它可以理解为先获得了lvalue*x,然后对这个lvalue进行了borrow。之所以称其为reborrow,是因为该lvalue所在的place实际上已经被xborrow过了,我们又「重新borrow」了一次,reborrow的特殊之处就在于Rust允许我们borrow一个已经被borrow过的lvalue。

更进一步,形如&(*ref).x的borrow也是允许的,也可以视为一种reborrow,即我们可以单独reborrow一个已经被borrow过的lvalue的一部分。

实际上reborrow的行为也是有其适用范围的:


需要注意的是,当我们使用&mut *xreborrow了x后,我们就将无法使用x,直到该reborrow expire为止,例如下列代码就是错误的:

1
2
3
4
let a = &mut 3;
let b = &mut *a; // a is reborrowed
*a = 4; // a is shadowed, ERROR
*b = 5; // reborrow expired

应该说,引入reborrow后,每个memory location都会有一个borrow stack。将borrow stack定义在memory location上而不是place上,是因为memory location没有进一步的结构,可以简化讨论,而place可以有子place,其不同的子place可以被分别reborrow,于是构不成一个stack。

borrow stack的规则很简单:

这里忽略了具有interior mutability的shared reference以及raw pointer,真正用于定义Rust语义的borrow stack模型要复杂得多,因为需要考虑unsafe特性与mutable/shared reference的语义的交互。

注意有时实际上可能发生reborrow产生的新reference尚且存活,旧的reference却已经被销毁的情况:

此时我们并不会立即将旧的reference从borrow stack中移除,而是会等待栈顶的reborrow先expire,再将旧的borrow移除。