`&&...&A -> &A` always safe?

Such a situation can occur when pulling &str out of containers, and you end up with a &&str. Is it always safe (and possible?) to reduce some arbitrary amount of reference layering down to a single one (in this case, a &&str to a &str)?

That's doable in safe rust (****r), so this is necessary always safe.

Care must be taken to use the best lifetime though. &'a &'b str can be reduced to either &'a str or &'b str. The latter is more general. Sometimes it's not clear what exactly is happening due to lifetime elision.

5 Likes

Is * the same as .deref()? * looks somehow dangerous and copyish to my eyes, given that * on primitives does a copy. Should I trust * more?

The simple answer is "yes, * is deref, and that's why you shouldn't do .deref()". The "well, actually" answer will involve deref coercions and other fun facts about method resolution.

Should I trust * more?

Yes. More generally, if you don't touch unsafe, the worst that can happen to you is coding yourself in a corner with wrong lifetime choices, and a long refactoring thereafter to thread the proper 'a everywhere.

copyish to my eyes, given that * on primitives does a copy.

This is exactly what is happening here.

fn f<'a>(x: &&'a str) -> &'a str {
  *x
}

copies-out the inner reference, exactly the same way as if x were an &usize. &T is a Copy type.

3 Likes

Is a reference just a cheap struct holding a raw ptr, etc? Is there a Rustonomicon section I could look at to learn more?

I am sure they are implemented as pure pointers.

The reference is a built-in type. You can think about it as

struct Ref<'a, T> {
    ptr: core::ptr::NonNull<T>,
}

impl<'a, T> Cloe + Copy for Ref<'a, T> { }

It will be almost accurate, but there's a snag preventing this from working -- you'll need phantom data to explain variance of &'a by example, and you'll end up with a circular definition

struct Ref<'a, T> {
    ptr: core::ptr::NonNull<T>,
    // urgh, I am trying to *define* a `&'a T` :-(
    ghost: core::marker::PhantomData<&'a T>, 
}

But, still, "reference is type ctor with one type parameter -- lifetime" is a useful intuition.

I don't think there's a great single source to learn such low-level details, but just looking at std::mem::size_of of things can often illuminate their inner workings.

1 Like

Just for completeness sake: When the target type is clear to the compiler, coercing from &&..&T to &T can happen totally implicitly in Rust, too:

fn foo<'a, T>(x: &&&&&'a T) -> &'a T {
    x
}
fn bar() {
    let x: &str = "hello";
    let y: &&&str = &&x;
    let z: &str = y;
}
fn f(_: &str) {}
fn baz() {
    let x: &&str = &"hi";
    f(x)
}
4 Likes