Can a struct that own an instance of a struct, also maintain a reference to this instance without a generic lifetime

Hi everyone, I’m new to this forum so be kind with my ignorance ;P.

I’m writting my first program in rust and I pretty much get the compiler work… But I’m not satisfy with my code… So here I am to ask for advice…

Let’s directly explain my problem with an example (which will be simplified the most I can):

First I have a struct A : struct A{}… dead simple.
Then I have a struct B that reference a A : struct B<'a> {a: &'a A}. Still pretty simple.
Then I have a struct AAndB that contains… a A… And a B… the subtlety is that B reference the same A than the one that is in AAndB :

    struct AAndB<'a> {
        a: A,
        b: B<'a>,
    }

    pub fn new_a_and_b<'b>(a: A) -> AAndB<'b> {
        AAndB { a, b: B { a: &a } }
    }

but here I got several problems :

  • AAndB need a lifetime… but in practice the lifetime of A in B is the same as A, in AAndB, so specifying a lifetime to AAndB is strange…
  • I cannot write this kind of code because of this generic lifetime :
    impl AAndB<'a> {
        pub fn new<'b>(a: A) -> AAndB<'b> {
            AAndB { a, b: B { a: &a } }
        }
    }

because 'a is not used…

So here is my questions :

  • can I write something like :
    struct AAndB {
        a: A,
        b: B<'self>,
    }

that would allow me not to expose a lifetime… that is in fact the lifetime of the object itself.

  • btw, how to do you name an instance of a struct ?
  1. I’m a rust beginner myself, so take this with a giant grain of salt.

I think I’ve run into this exact issue in the past.

  1. What I INCORRECTLY want let ans = AAndB { a, b: B { a: &a } } to do: have ans.b.a be a ref to ans.a

  2. What is actually happening: ans.b.a is referring to the (a:A) passed in as a function argument. Unfortunately, the function argument dies at the end of the function call, and is MOVED to ans.a

  3. Thus, the ‘lifetime issue’ is ans.b.a is trying to refer to something that won’t exist at end of function call.

2 Likes
  1. What I INCORRECTLY want let ans = AAndB { a, b: B { a: &a } } to do: have ans.b.a be a ref to ans.a

If i understand well, that s what I’m trying to do…

  1. What is actually happening: ans.b.a is referring to the (a:A) passed in as a function argument. Unfortunately, the function argument dies at the end of the function call, and is MOVED to ans.a

I don’t get it… ok, a is moved to the function… but there it is moved into AAndB right ? so… it’s not freed at the end of the function ?
– edit : ok it’s not moved into struct…

  1. Thus, the ‘lifetime issue’ is ans.b.a is trying to refer to something that won’t exist at end of function call.

if compile for me, so I assume this is not the case =)
– edit : ok … My bad it’s not compiling… you are right…


btw I can write the constructor like this (it was just a misplaced generic placement) :

    impl <'a>AAndB {
        pub fn new<'b>(a: A) -> AAndB<'b> {
            AAndB { a, b: B { a: &a } }
        }
    }

do you guy consider this constructor ok ?

3 Likes

In Rust, every move is by definition a shallow memcpy, so single move will invalidate every references to the previous value. Or strictly speaking, It is compile error to try to move a value while at least one of references that points to this value.

Let’s see your original code, fn new_a_and_b(). You’re trying to store reference of the argument a, which is already moved out into your newly created struct. Ok, let’s assume you did somehow correctly referenced tha a, a field of the struct. Now a field a of the struct is borrowed, you can’t return the struct, as return is also a move!

Generally speaking, using struct that contains borrowed reference is hard. And using self-borrowing struct is much much harder, just next to theorically impossible. Good news is: most problems that seems to need self borrowing are solvable using Rc/Arc. It takes a single heap allocation as a cost, but provides guaranteed memory safety as a return.

1 Like

reading shepmaster post (that seems accurate), then I’m gonna look at Rc/Arc… that i don’t know…

so from what I understand I have 3 choices:

  • changing my design to avoid self borrowing struct… but it’s not working in my case (b as other data than just a in reality
  • using Rc/Arc, this is the simplest solution but comes with extra calculation at creation/drop of a… which should be ok but still…
  • using a Pin<Box<A>> and a NonNull pointer with defered initialisation… which is the closest to what I wanted to do… but much more complex…

edit: (later)
so I tried to implement the 3rd solution :

#[derive(Debug)]
struct A {}

#[derive(Debug)]
struct B {
    a: NonNull<A>
}

#[derive(Debug)]
struct AAndB {
    a: Pin<Box<A>>,
    b: B,
}

impl AAndB {
    fn new() -> Self {
        let mut result = AAndB { a: Box::pin(A {}), b: B { a: NonNull::dangling() } };
        let ra = result.a.as_ref().get_ref();
        result.b.a = NonNull::from(ra);
        result
    }
}

is that correct ?


edit:
hum… I thought that if b is moved out of AAndB… then it should break… So I wrote a test & it did not break… :

impl A {
    fn get_stuff(&self) {
        println!("get stuff");
    }
}

fn b() -> B {
    AAndB::new().b
    //I expect AAndB instance to be droped here... and the same for A in it... so b.a.as_ref should be... panic...??
}

fn main() {
    unsafe { b().a.as_ref() }.get_stuff(); // get stuff is printed...
}

i’m not sure… Maybe compiler link lifetime of AAndB to AAndB.a and also to AAndB.b… so nothing is droped or everything is dropped ???

I’m getting confused on what I thought I understood…

No, because you are unsafely converting the pointer ina into a reference Rust trusts you that it still points to a live object (it cannot verify this because pointers don’t carry lifetimes). Since it doesn’t what you have here is a classic use-after-free producing Undefined Behaviour; so while it appears to work currently, it’s free to do anything at all in the future.

I would highly recommend not trying to use Pin for self-referential structs yet if you’re just starting to learn Rust. The guarantees provided by Pin and how you can then soundly use it to make a struct self-referential are rather subtle and are missing a lot of documentation (and they are very unergonomic to work with).

3 Likes

To add to Nemo157’s point – given you’re already using unsafe you might be running into the brick wall of “here’s how I want to write my code; let me make Rust adjust to me” instead of asking “what’s idiomatic Rust borrow/lifetime/ref code?”

I ran into this wall a few months ago, and the way I got around it was as follows:

If I accept “Rust is safer than C/C++”, then there must be certain “design patterns” that are valid C/C++ but invalid Rust – and I have to be willing to give those up and ask “What is the idiomatic Rust way to solve problem X?” rather than “Can I make Rust implement solution Y”

8 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.