Reborrow "function" vs reborrow operator

I'm trying to learn more about the details of reborrowing. There was a useful thread about reborrowing recently, but replies are locked. So I'll open a new thread.

Inside the linked thread was a link to a blog post. The blog post attempts to define reborrows in terms of a function:

fn reborrow<'a, 'b, T>(r: &'a mut &'b mut T) -> &'a mut T {
    r
}

In the same thread QuineDot mentions:

The links in their reply goes to a function foo:

fn foo(s: &mut String) -> &str {
    &**s
}

or:

fn foo(s: &mut String) -> &str {
    let ms: &mut str = &mut **s;
    let rs: &str = &*s;
    rs
}

Why is this distinction important? Is the idea that foo cannot be rewritten in terms of reborrow because the reborrow operator is special and can handle e.g. locals going out of scope?1

Additionally, why in the reborrow function, why is the language "the reference itself is borrowed," as opposed to reborrowed? My understanding of this function when called like e.g:

let mut bar = String::new();

let mut quux = &mut bar; // Avoid a temporary dropped error.
let baz = reborrow(&mut quux);

is something like this:

  1. quux has type &'b mut String. Create a temp binding for &'first mut quux.
  2. Immediately reborrow that temp binding, e.g. do (&'a mut *(&'first mut quux)). r has type &'a mut &'b mut String.
  3. The first/only expression in that function is to return s. The return type and s don't match. 4. Deref to match the nesting level of references. This gives us a &'b mut String.
  4. &'a mut String is a subtype of &'b mut String, so coerce to &'a mut String.
  5. s is moved to baz and s goes out of scope.
  6. When baz goes out of scope (lifetime 'a), the lifetime &'first introduced by the temp binding goes out of scope as well. quux binding is usable again.

Is the above more-or-less correct? If so, what makes the reborrow operators in foo (either version) have different semantics than what I just listed?1

  1. I hope I'm somewhat on the right track. Otherwise, I don't think I understand specifically why that section of QuineDots (very useful!) "Learning Rust" book is linked. I can't tell whether function foo itself is meant to compare-and-constrast directly with reborrow, or foo contains expressions/operators that a reborrow function cannot convey.

I'm bad at short replies, sorry. I'm going to reply to your post and then I'm going to try another tact to highlight the difference between a reborrow "function" and an actual reborrow. I'm not sure which would be better to read first.

Actual reborrows can handle references going out of scope in ways that reborrow cannot, yes.

It's a borrow-check error if something goes out of scope when it is borrowed. So in the second version of foo, we can conclude that s and ms are not borrowed when the function returns and s and ms go out of scope. There isn't the equivalent of some &mut s or &ms hanging around that rs depends on.

Instead of s being borrowed, **s is borrowed. And instead of ms being borrowed, *s is reborrowed. This still imposes lifetime constraints -- in this case, the lifetime in the type of rs ends up with the same lifetime that is in the type of s, and uses of the returned rs will keep the String borrowed. But because ms and s are not themselves borrowed, they are allowed to go out of scope. The lifetime constraints enable the borrow checker to keep everything sound without requiring the references themselves to stick around.

The same sort of thing happens in the first version of foo (s goes out of scope, but a reborrow through s can stay alive).

Because there, the (inner) reference itself is borrowed -- there's a reference to a reference.

//                        vvvvvvvvvvvvvvvvv
fn reborrow<'a, 'b, T>(r: &'a mut &'b mut T) -> &'a mut T { ... }
// ...
let baz = reborrow(&mut quux);

We're not reborrowing *quux here, we're borrowing quux itself.

Places get borrowed, and *quux is a different place than quux.

If quux has type &'b mut String, the borrow on the right here can technically be something longer like &'first mut String...

let mut quux = &mut bar;

...but in practice the lifetimes are going to end up the same. It's usually easier to just talk as if they were the same from the start, but we'll see a variation on this in slow motion in a moment.

The types of variables don't change, and &mut X is invariant in X. So the expression &mut quux that is passed to reborrow is a &'infer_me mut &'b mut String. It's not a reborrow, it's just a borrow of quux.

Did you mean r here? We're still talking about the reborrow function? I'll assume so.

The deref coercion here is more like &mut **r. r is not moved -- it's the wrong type and wrong address to be what is assigned to baz. And certainly *r is not moved: you cannot move values from behind a reference, and &mut _ are not Copy.

The newly created &mut -- the reborrow -- has a new lifetime, with constraints. In this case, the lifetime constraints from the reborrow say that 'b: 'reborrow and 'a: 'reborrow. We also need 'reborrow: 'a to be able to coerce to the return type. So the lifetime in the type of reborrow ends up needing to be 'a exactly. (As with the assignment of quux, it's sometimes easier to take a shortcut and just say the reborrow is 'a from the start. But I wanted to highlight the reborrow constrains.)

Let's consider a few signatures:

fn foo_but_mut<'f>(s: &'f mut String) -> &'f mut str { ... }

fn generic<'g, Referent, Res>(input: &'g mut Referent) -> &'g mut Res
    where Res: ?Sized { ... }

fn reborrow<'a, 'b, T>(r: &'a mut &'b mut T) -> &'a mut T { ... }

As I said before, uses of the return of foo keep the String borrowed -- that's the meaning of the lifetime relationship between the input and the output. That's the meaning of every function with an API like that: with generic, use of the returned &mut Res keeps the Referent borrowed. foo_but_mut is like generic; we just replaced Referent with String and Res with str.

And it's true of reborrow too: replace Referent with &'b mut T and Ref with T. So use of the return value keeps the &'b mut T borrowed. It keeps the reference itself borrowed. A local reborrow could keep *quux borrowed without keeping quux borrowed, but this function signature cannot do that. It has to keep quux itself borrowed.

Be careful, Rust lifetimes like 'a are about borrow duration, not liveness scopes. And the lifetimes are also non-lexical.

But once baz has no more uses, it stops keeping quux borrowed, yes.


Okay, here's where I try a different tact. This compiles:

    let mut s = String::new();
    let reborrow: &mut String = {
        let r = &mut s;
        &mut *r
        // `r` goes out of scope here.
    };
    reborrow.push('h');

Let's say r has type &'s mut String. Uses of values with types containing 's will keep the lifetime 's alive, which keeps the borrow of the String s alive.[1]

And let's say the reborrow expression &mut *r has lifetime 'r. There is a reborrow constraint: 's: 'r. That means uses of 'r will keep 's alive.

Note: *r is reborrowed. r is not, itself, borrowed.

r then goes out of scope. Going out of scope when borrowed results in a borrow checker error, but there is no error here, because r is not borrowed.

Then we assign to reborrow. We could introduce another lifetime here, say 'block, and the assignment would require 'r: 'block -- so uses of 'block keep 'r alive, which keeps 's alive, which keeps s borrowed. But as with the other examples, it's simpler to just say reborrow has lifetime 'r. Either way, we have to move the &mut *r value to perform the assignment, which is a use of 'r outside the inner block.

After that, uses of reborrow keep s borrowed via the chain of lifetimes, which is required for soundness. The reborrow is still reusable after the inner block, even though r went out of scope.

Now let's see what happens when we try something similar, but using a nested reference. This does not compile:

    let nested_attempt: &mut String = {
        let mut r = &mut s;
        let outer = &mut r;
        &mut **outer
        // `r` and `outer` go out of scope here
    };

Let's say r has type &'s mut String, as before. And let's say that outer has type &'r mut &'s mut String. There's an implicit constraint: 's: 'r. Uses of 'r will keep 's alive.

Note that unlike before, r itself is borrowed. Uses of 'r will keep r borrowed.

Let's say the &mut **outer reborrow has lifetime 'x.[2] The reborrow through &'r mut &'s mut introduces two lifetime constraints: 'r: 'x, 's: 'x. Uses of the reborrow keep 'x alive, which keeps 'r and 's alive... which keep the borrows of s and of r alive.

As before we have to move the value to do the assignment. That's a use of 'x outside of the inner block, which keeps all those lifetimes and borrows alive. That means that r is still borrowed when it goes out of scope... and that's why you get a borrow checker error.

28 |         let mut r = &mut s;
   |             ----- binding `r` declared here
29 |         let outer = &mut r;
   |                     ^^^^^^ borrowed value does not live long enough
30 |         &mut **outer
   |         ------------ borrow later used here
31 |     };
   |     - `r` dropped here while still borrowed

(outer itself was never borrowed.)


Here are the examples in the playground.

The nested example corresponds to the fn reborrow attempt. It requires keeping an intermediate reference -- in most examples, the original &mut OuterThingYouCareAbout -- borrowed. Which requires keeping it around.

Whereas with actual reborrowing, you can reborrow through a reference without borrowing the reference itself, and then let the reference go out of scope.


Finally, let's consider a practical example which relates to the distinction between actual reborrowing and emulated reborrowing via nested references. Instead of directly working with nested references, we'll consider something like a &Option<&mut T>, where there's an intervening wrapper type.

Wouldn't it be nice if we could have something like...

fn reborrow_opt<T>(opt: Option<&mut T>) -> Option<&T> { ... }

...but, instead of passing in opt by value and thus losing it forever, something akin to a reborrow happened -- so you could still use opt afterwards?

Well, you can do this today...

fn example(opt: Option<&mut String>) -> &str {
    let tmp: Option<&String> = opt.as_deref();
    // ...

This gives us the "didn't move opt" part. You can use opt again once tmp goes away. But similar to the reborrow function we looked at above, as_deref ends up taking some form of nested reference,[3] and opt itself remains borrowed so long as tmp is in use...

    // ...
    tmp.map(String::as_str).unwrap_or("")
}

...which causes the attempted return to fail as the local opt is now still borrowed when it goes out of scope. So this approach does not have the "original can go out of scope" property that reborrows have.

There's currently no function API that can emulate reborrowing for Option<&mut String>. Reborrows are, to date, a reference-specific mechanism. So you have to locally drill down to get to the reference (like with a match or something), instead of having something nicer like a method. If you try to use methods, you end up with something like fn reborrow or as_deref, which don't solve all use cases.

There's a lang experiment to explore some sort of Reborrow trait to enable reborrows through wrappers like Option. The idea is that you could pass an Option<&mut T> somewhere and it wouldn't necessarily be moved. Instead of moving it, a reborrowed Option<&mut T> could be passed, like what happens with &mut T. There would be lifetime constraints for soundness, but the Option itself would not be borrowed.

If that experiment is successful, you could use the reborrow_opt function above to get the reborrow we desired -- without even requiring some new kind of function API.[4]


  1. From here on I'll say things like "has lifetime" and "uses of a lifetime" instead of spelling out "uses of a value whose type contains a lifetime" for brevity. ↩︎

  2. yeah I ran out of good names, sorry ↩︎

  3. it takes &Option<&mut String> here ↩︎

  4. It would be like the match in example_2 of my last playground happened automatically. ↩︎

1 Like

I appreciate the well-thought-out reply. Please understand that it's probably going to take me a while to understand the whole thing. I don't think I'll be able to put all follow-up questions in one single reply (and it's probably not a good idea to anyway). For starters:

Yes, I meant r. Transcribe fail there.

Could you elaborate? In my example I intended, &'first as a hypothetical lifetime that's conjured as part of creating the &mut quux (which has no named binding) in the first place. So I don't understand how &mut bar can get the lifetime of 'first; as I intended to convey, which of course could still be/probably is wrong, the 'first lifetime doesn't exist before the expression &mut quux in the source code! So why does it back-propagate back to the lifetime of &mut bar rather than form a stack of borrows?

I thought all functions with mut &'whatever parameter perform a reborrow. But you're saying that in this particular reborrow(&mut quux);, no reborrow at all occurs, even as the initial part of binding to r inside reborrow?

FWIW, I've decided to read in-order. E.g. focusing on the first half first.

Ah yeah, I misunderstood you there. Sorry about that. IIUC now, you were talking about:

//                           &'expr_3 mut &'_ mut _
//                           vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
let baz: &'a mut &'_ mut _ = reborrow::<'call, '_, _>(&'expr_2 mut quux);

(Right?)

And the followup question is, does *&'expr_2 mut quux get reborrowed? Yes, my understanding lines up with yours: this always happens when &mut _ is in the signature.

Technically all of the named lifetimes in that snippet could be different so long as the coercions work out:

'expr_2: 'call: 'expr_3: 'a

In practice things will act as if they're all the same, unless something like generics or annotations interfere with reborrows or inference, or the like. Because the only thing that has an effect on 'expr_2, 'call, and 'expr_3 after the assignment is 'a being in the type of baz.[1]

(Due to misunderstanding, I talked about...

let quuz: &'b mut _ = &'expr_1 mut bar;
// 'expr_1: 'b

...because I thought that's what you meant by 'first.)

I think this was just confusion caused by my misunderstanding you, and talking about something different. But if there's still an unanswered question here, feel free to point it out.


  1. I'm not actually sure if all of them get distinct lifetimes in the compiler implementation or not. ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.