Double references what they mean

While read some learning materials I found this :slight_smile:

let z: &'c &'b i32 = &'c y;

Which I can guess what it's mean but I prefer 100% accurate explanation what it's mean.
Also while search in internet what double reference can mean I found context where people use double references like &&. What reference stacking mean. Did && is different toke like c++ where && mean "movable" object or it's just stacking

Double reference is nothing special in Rust. In particular there’s no &&-token-having-special-meaning weirdness going on like in C++, instead a (prefix) && token has the same meaning as two seperate tokens & &.

For any type T, you can create the type of references to T, that is, &'a T. When T itself is the type of references to yet-another-type S, you can have &'a T being &'a &'b S. In case of S being i32, we’re back at your example. The implementation of something like &'c &'b i32 is with a pointer that points to yet another pointer whch in turn points to an i32-value.

You can dereference z: &'c &'b i32 back to a &'b i32 by writing *z. The underlying i32 then would be accessible under **z. Rust does have some priciples that can operate through multiple levels of indirection. For example, you can still call &self methods of a type S in a double reference &&S or even a triple reference &&&S, etc. Something like

let x: Option<i32> = Some(0);
// how about QUINTUPLE references?!?
let y: &&&&&Option<i32> = &&&&&x;
let z: bool = y.is_some();

works just fine. Rust also allows has implicit conversions (so-called “coercion”) that can remove extra layers of references, e.g.

let x: Option<i32> = Some(0);
let y: &&&&&Option<i32> = &&&&&x;
let z: &Option<i32> = y;

There’s also certain considerations on how exactly lifetimes work with multiple levels of shared (&) and/or mutable (&mut) references. E.g. sharing a mutable reference via &&mut T obviously doesn’t give you unique access to the underlying T anymore. And in a &'c &'b i32 reference, the inner lifetime 'b must always be longer than the outer lifetime 'c.


By the way, something like

let z: &'c &'b i32 = &'c y;

is not actually valid Rust code. You might see this as pseudo-code somewhere when people try to explain lifetimes, but you cannot introduce (or prescribe) the lifetime of a reference in the way that the expression &'c y seems to do.

17 Likes

Great explanation. Is there a typical/canonical use-case for borrowing a borrow?

I can imagine where it comes to be, but less “by design” per se. What I mean is, in C a pointer to pointer is useful by design to access say an array of strings... &Vec<&str>?? , or &[&str]just doesn’t seem to resonate.

Why is that?

There’s certainly more value in something like &mut &mut T or even &mut &T than &&T. I guess in some situations double references like this can come up implicitly through generic code.

One example that I could imagine is some nested calls through functions that are generic over some F: Fn(), receiving f: F by-value and then passing &f along to other functions that need some G: Fn() closures themself.

Another example, one of &mut &T, that comes to mind is the &mut &[u8]-typed self argument in a certain Read implementation.

Something like &Vec<T> may also count as a double-indirection, as Vec is pretty much a smart-pointer type. For the same reason why &Vec<T> is discouraged for &[T], one should probably prefer &T over &&T when it’s possible.

1 Like

They come up in iterator adapters for example.

let v = vec![3, 1, 4];
for _ in v.iter().filter(|_x: &&isize| true ) { }
2 Likes

Why? I have written plenty of test cases for parsers using something like

let test_cases: &[&str] = &[
    "foo",
    "bar",
    "baz",
];
2 Likes

I see that the docs confirm that what you're saying is no mistake. But for the life of me, why is it allowed to mutably borrow from a shared borrow? My intuition tells me that that should be forbidden by borrowck (i.e. it should be impossible to get writable and exclusive access based on an immutable, shared borrow), so precisely how is this sound?

1 Like

Shared borrow is no different from any other type in this case.

It's indeed impossible to get writable and exclusive access to the underlying data, since the original shared borrow is still here. But it's possible to get writable and exclusive access to the borrow itself - and replace it with borrow of another data, for example.

4 Likes

Or more concretely (playground).

4 Likes

The answer from @Cerberuser made it obvious to me, but this certainly doesn't hurt either.

The thing that made it click for me was the part about changing the referent of the borrow, which made me think about types like size_t** in C and what such types would be used for, i.e. most often it's to track and update a pointer rather than the value behind it. Something like &mut &T is no different in that regard.

I'd say the biggest difference is that in my 5 years of professional software development using Rust (contrary to in C) I've never had to use a pattern like this, and on top of that came my reasoning of what borrowck would do, which confused the hell out of me.

1 Like

It's not mutably borrowing from a shared borrow. It's mutably borrowing a shared borrow itself. I.e. &mut &[u8] is a pointer behind which the inner pointer-to-slice can be reassigned, so that it points to another location. You still can't use this to change the bytes themselves.

In fact, the cited Read impl does just this: it chops off the first n bytes and adjusts the slice so that it begins with subsequent elements.

2 Likes