Return reference with lifetime bound to a struct's field instead to the struct's

This might be a bit of a beginner question, but I would really appreciate the help.
I have a situation in which I have a struct B that contains a field of type A.
However, A comes from an external crate and has a complex, hard to use API.
Therefore, my instinct coming from C++ would be to wrap A's API in a minimal set of methods implemented for B.

This has led me to the following code:

struct A {
    pub x: i32
    // Lots of other fields
}
impl A {
    fn get_x_ref(&self) -> &i32 {
        &self.x
    }
    // Lots of other methods
}

struct B {
    a: A,
    pub y: i32
}
impl B {
    fn get_x_ref(&self) -> &i32 {
        &self.a.get_x_ref()
    }
}

fn main() {
    let mut b = B { a: A { x: 0 }, y: 0 };
    let x = b.get_x_ref();
    b.y += 1;
    println!("{}", x)
}

Which is logically sound but of course gives a compilation error since we are borrowing b by getting a reference to b.a.x, and then trying to borrow it again to increase b.y.

I know of a few solution to this, namely what was described here: After NLL: Interprocedural conflicts · baby steps

But all solutions require code refactoring for a problem that i don't think stems from bad code design. And if there is something I really dislike is thinking hard about my code structure only to have to refactor because the compiler is screaming at me.

So my question is: should this design pattern be avoided in Rust because it promotes this type of error (and, in this case, what should it be replaced with?), or are there solutions to this that don't require refactoring "hacks" (either currently, or in progress)?

It seems a shame that a language that incentivises composition as a pattern (as opposed to OOP's inheritance mess) would not have a drop in way to fix this, something that would allow us to tell the compiler that &x is bound to A and cannot be changed by methods of B that don't interact with b.a.

Can you clarify what kind of visibility you actually want? In the example, all your fields are public but all your methods and the structs themselves are private.

One way forward might be something like

// Implement your minimal, less-fiddly API on this
pub struct SaferA(A);
impl SaferA {
    pub fn get_x_ref(&self) -> &i32 { ... }
}

pub struct B {
    pub a: SaferA,
    pub y: i32,
}

// ...

    let x = b.a.get_x_ref();
    b.y += 1;
    println!("{}", x)

The relevant diff for the error[1] is

-    let x = b.get_x_ref();
+    let x = b.a.get_x_ref();

which works as borrows of fields are tracked separately. But it's only an option if the fields are visible.

(Borrow checking is a significant reason why accessing fields directly is more idiomatic than using getters, when the fields are visible.)

New function APIs that can specify what fields are borrowed how -- like the view types from that blog post, but as an API and not a nominal struct -- get talked about from time to time. Sometimes this is called "partial borrowing" or sometimes you just have to search for "view types". But there's no accepted RFC or implementation, so they are a long ways off at best.

It's still a type of refactoring as a different API is a different commitment. Some of the discussion includes how public such APIs could be, etc. It's challenging to commit to a public API that only interacts with field a if field a is a private field / implementation detail.


  1. which can be applied to your OP code too ↩︎

1 Like

In this case b.a would indeed be most likely private, since the point is exactly hiding the quirks of its implementation behind B. I'll have a look at partial borrows, thanks for the tip

Here's another post by Niko about that idea, and it comes up on internals from time to time, and there are a few RFC attempts. There's a lot of conversation but not much actual movement so far.

2 Likes

Wrapping A in B to provide a nicer API should be fine, and any limitations will be the limitations of A, which you have no control over.

I think the problem is that you haven't just wrapped A in B, you've also added more fields to A (y). And then you're trying to use a borrow via &self (A), while at the same time mutating A::y, which is not possible due to Rust's basic borrowing rules (shared XOR mutable), as you've discovered. Instead:

  • don't add more fields to A, or
  • don't return borrows via A that cause conflicts

It is a very common mistake in Rust to bundle together fields in a struct, for convenience for example, that don't need to be bundled. The solution to borrowing conflicts is often to keep them separate, even if this means more function parameters must be passed or more variables must be declared locally.

Or perhaps you do need to bundle for some reason that isn't apparent from your example. Your example is very abstract, presumably because you're trying to simplify your question. But if you can show the actual code you're trying to write, you'll probably get better advice.

1 Like