Struct method and borrow checker

Hi,

I'm new to rust and facing a compilation problem i'm unable to solve (yet another borrowed issue :).

Consider the following basic example of a structure with a method returning a reference to one of its fields.

struct Foo {
    a: u16,
    b: u16,
}

impl Foo {
    fn get_a(&self) -> &u16 {
        &self.a
    }
}

fn main() {
    let mut f = Foo{ a: 1, b: 0 };

    // ex1 compile
    let a = &f.a;
    f.b = a + 1;

    // ex2 fail
    let a = f.get_a();
    f.b = a + 1;

    // ex3 compile
    f.b = f.get_a() + 1;

println!("a {} b {}", f.a, f.b);

}

Why the hell "ex2" fails to compile (E0506: cannot assign to f.b because it is borrowed at f.get_a()), while "ex1" and "ex3" compile ?

Is there any internal difference interpreted by the borrow checker ? In each situation, a reference to "a" is retrieved either directly from the field or via a method and used to compute some new value to another field.

Thanks in advance for your support.

1 Like

let a = f.get_a().clone();

ex3 case is obvious: you borrow whole f while calling f.get_a(), then add something to the result, then the borrow is no longer needed and may be forgotten so f may be borrowed again for assignment.

ex2 doesn't compile work, because get_a elided lifetimes to get_a(&'a self) -> &'a u16, so the f must be borrowed for at least a long, as returned result, so nothing from f may be borrowing until a is in scope. In this case you know, they only thing this function using internally is f.a, but it can't be deduced looking at function signature.

In ex1 you never borrow whole f - you are just borrowing distinct part of it.

When you operate on a "bare" struct, then the borrow checker sees each field individually, and allows you to borrow and modify individual fields independently.

When you call a method, the body of the method is invisible to the borrow checker, so all it knows is that the whole object (&self) is borrowed, so it assumes all struct fields are in use.

When you run let a = f.get_a();, then f (with all fields) is borrowed as exclusively read-only for as long as a is in scope — here until the end of the function.

The workaround is to limit how long a lives by putting it in a smaller scope {}:

let tmp = { 
  let a = f.get_a();
  a + 1
}; 
// here `a` doesn't exist any more, so `f` is not borrowed any more either
f.b = tmp;
2 Likes

Thanks guys,

Got it. Very clear and helpful.

Is there anyway to change method signature so that Rust knowns that i'm only borrowing part of F like in "ex1" and not whole F as when i operate on "bare" struct ?

I mean, using lifetime or something else ? Or this is considered bad Rust habit and should be written in another fashion (as in your workaround). Is this the right coding pattern ?

Unfortunately there's no way to do this for methods. Partially it's just a limitation (nobody proposed a compelling syntax and implementation for this), and partially it's by design: it makes your implementation hidden, your object's API simple, and gives you an option to use more fields later without breaking any uses of the method.

You may need to rethink the API. You could make fields public (perhaps OK if that object is simple like a Builder pattern).

You could provide specialized methods so that users don't need to borrow the field (instead of get_a() + 1 have add_1_to_a()).

You could split f into two separate types.

You could make a a Copy type (no borrow needed). If it's undesirable to copy it, then you can wrap it in RefCell, which checks borrows at run time, which is much more flexible.

1 Like

More specific disjointness has been proposed/discussed a few times. Here's one example: https://github.com/nikomatsakis/fields-in-traits-rfc/issues/16

Actuality this "workaround" is common Rust pattern, and if it works, use it. However if you really need two distinct borrows in one time, consider creating function like:

split(&mut self) -> (&u16, &mut u16) {
    (&self.a, &mut self.b)
}
1 Like

Does the Copy trait will be faster than explicitly creating a 'tmp' storage, as the compiler will decide on its own if it needs to borrow or copy ? I think RefCell can induce overhead in such a simple situation no ?

Basically i understand that learning Rust is also learning a new way to write code.
I actually wrote get_some_field() to return a reference to a field which is an Option.

I wanted to hide the fact that field "a" could be None and prevent a "match" anywhere i wanted to use it.
So i decided to write a "getter" that match on the field, if None initialize it and in any situation returns a reference to the field real content. Any other method of the Structure which want to access a reference to that field won't have to know it's based on an Option.

I decided using Option for some structure fields that are also structures from extern crates, that does not have Default trait and can hardly be initialized at declaration (ie. raw CPUID ExtendedFunctionInfo).

ok and doing some

let (a,b) = f.split();
*b = *a + 1;

in a { } to limit mutable borrow scope to be able to further create references to F fields (immutable for instance to print them).

It's the same principle as Kornel tmp storage actually :slight_smile:

In simple case yes, but I assume this is simplification, and the real world is somehow different. If you need b borrowed mutably, you may need it for something more sophisticated, eg call mutating method on it - in such case it let's you avoid unnecessary copy.

Also in case where you need two distinct immutable borrows this way you may take them, without any copies.

You can "invert" the direction by exposing a method that takes a closure which is passed a reference to your field. This way the reference to a field doesn't escape out of a method, and thus doesn't extend the borrow (over the entire struct).

Copy for small types (<= 8 bytes) is likely to just as fast or faster than using references. For bigger types it depends how much copying happens. The compiler will optimize out copies in many cases, so you should be primarily concerned whether Copy is semantically correct for the type (if it's just a number, then having lots of copies is fine, but OTOH if it's a file handle, then implicit copies would be very surprising). Assignment to temporary variables is usually free.

3 Likes

Hi, so why does this code compile on my machine fine?
what changed?

In 2018 Rust has released an improved borrow checker that understands more complicated code.

1 Like