How to handle a reference in a struct to another member in the struct

Hello Community,

I am currently learning rust and I have a problem I do not know how to solve.
I have a struct containing several owned members and a reference, which is supposed to point to one of these members. Here is some example code (minimal example with no real world purpose):

struct Test<'a> {
   a: u32,
   b: u32,
   reference: Option<&'a u32>
}

impl<'a> Test<'a> {
   fn new() -> Test<'a> {
       Test {
           a: 1,
           b: 2,
           reference: None
       }
   }

   fn set_a(self: &'a mut Self) {
       self.reference = Some(&self.a);
   }

   fn print(self: &Self) {
       println!("a={}, b={}, reference={:?}", self.a, self.b, self.reference)
   }
}

fn main() {
   let mut test = Test::new();
   test.set_a();
   test.print()
}

When I compile this code as this, I get:

error[E0502]: cannot borrow `test` as immutable because it is also borrowed as mutable
  --> src\main.rs:29:5
   |
28 |     test.set_a();
   |     ------------ mutable borrow occurs here
29 |     test.print()
   |     ^^^^^^^^^^^^
   |     |
   |     immutable borrow occurs here
   |     mutable borrow later used here

I noticed, that if I would not have to specify the lifetime annotation in:

   fn set_a(self: &'a mut Self) {
        self.reference = Some(&self.a);
    }

Than it would work in the main, but set_a does not compile anymore because self would get an anonymous lifetime.

But the logic should work, should it not? reference does not outlive a. But how can I explain that to the compiler?

Or is my error somewhere else?

This is known as a self-referential struct, and there is no good way to do it in Rust so far -- it does not play well with the ownership and borrowing semantics. If you search this forum for the term, you'll find many threads.

In the first case, you're mutably (i.e. exclusively) borrowing the struct for the entire lifetime of the struct. This makes it useless -- unusable -- after the call.

In the second case, you can't create a &'long mut T by going through a &'short mut U, as you can create memory unsafety by doing so.

Finding some approach that doesn't involve a direct self-reference is recommended (an enum, an index or Range...).

4 Likes

No, it shouldn't. Where do you think the reference would point after the struct (with all of its fields, including the referenced one) is moved? Since moving doesn't change the lifetime parameters, if this kind of code were allowed to compile, it would trivially result in use-after-free. However, it's – correctly – rejected by the compiler, exactly because the lifetime annotations simply can't be assigned in a general enough way for borrowck to allow this. This is fundamental.

You should instead consider what you are trying to achieve (at the higher level). Likely, there is a correct (i.e., memory-safe) solution for that problem.

3 Likes

This couldn't ever work as this cite from Rustonomicon explains:

Every type must be ready for it to be blindly memcopied to somewhere else in memory. This means pure on-the-stack-but- still-movable intrusive linked lists are simply not happening in Rust (safely).

I just wish it was somewhere in more accessible place, coz deep investigation of complex parts of Rustonomicon is not something newbies read and this they make mistake #3 insanely often.

Note that Rustonomicon, basically, tells you, tantalizingly, that it is possible if you use unsafe, and, indeed, std::pin makes it possible, but… that's just not something that you want to use when you are still learning Rust.

No, it's not problem of ownership and borrowing. They could have been adjusted, probably, if the whole thing made sense. But thing about function new in the topicstarter example: it first creates Test struct and the moves it to some other place. At this point you either have introduce move constructor and allow move to be non-trivial (like C++ did) or move object to the heap (like most GC-based language do).

First approach was deemed unfeasible (it's really hard to deal with constructors since they couldn't just return error and panic in the middle of function call is just too surprising) and box everything is not something you do in a system language. If you really need that, there's pinned box, but it requires unsafe and, in general, most definitely not something you try to learn Rust with.

2 Likes

Thanks for all the help guys! I understand now why this does not work.
Next I have to find out what to do instead :). I asked another question for that here: Should I use references for pointing to objects in another struct?

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.