Why does returning self cause problems with variables and deferred chaining?

#[derive(Debug)]
struct Chainer {
    a: i32,
    b: i32,
}

impl Chainer {
    fn new() -> Self {
        Chainer { a: 0, b: 0 }
    }
    fn set_a(mut self, a: i32) -> Self {
        self.a = a;
        self
    }
    
    // note: this function takes ownership of the receiver `self`,
    //       which moves `chainer_b`
    fn set_b(mut self, b: i32) -> Self {
        self.b = b;
        self
    }
    fn print(&self){
        println!("{:?}", self);
    }
}

fn main() {
    let chainer = Chainer::new().set_a(1).set_b(2);
    println!("Chainer: {:?}", chainer);

    let chainer_b = Chainer::new();
    chainer_b.set_b(100).print(); // This works
    
    // error[E0382]: borrow of moved value: `chainer_b`
    println!("Chainer B: {:?}", chainer_b); 
}

The builder pattern is used in many places, but I'm running into a problem where I want to build some parts in one place and then continue in another. The lib in this case is clap, so I can't modify the code.

What I'm trying to understand is why I'm getting the error. How come chainer_b doesn't get back ownership. Does it go

  • set_b becomes the owner of self
  • returns self to the caller/main() (?)
  • print() gets a reference to self (is self now a temporary variable in main() ?)
  • access to chainer_b fails because it's not the owner even if self is in the same scope?

The ugly solution I found was

let mut chainer_b = Chainer::new();
chainer_b = chainer_b.set_b(100);
chainer_b.print();
println!("Chainer B: {:?}", chainer_b);

It would be really nice if the intermediate code could be printed out with annotations of who owns what and who's borrowing who. I'm sure it would make these kinds of questions much less frequent. (Is it maybe already possible?)

You're getting the error because of this line:

Contrary to your comment, it doesn't work, giving only the appearance that it works.

This is because the call to set_b() takes ownership of self (this is the move that the error is talking about) and returns it, and then print() borrows it in order to print something. After this, the value that used to be stored in chainer_b (aka self) is dropped, and so the compiler complains when you try to use it again in the last line.

The potential solutions are:

  1. what you called the ugly solution
  2. Make the setters take and return &mut self rather than self

Not to my knowledge, no.

2 Likes

Because nothing moves the value back into chainer_b (which isn't mutable, so it can't be assigned a new value anyways).

Where your program calls Chainer::set_b and passes chainer_b as its first argument, Rust (following your specification) moves the value out of chainer_b. Logically, the variable contains nothing at that point, as the value it used to contain is now bound to the self argument of set_b.

set_b then returns that value. Since it's not moved anywhere else, the returned value is dropped at the end of the statement.

You are able to call Chainer::print on the returned value because Rust allows you to borrow temporary values, so long as the borrow would not outlive the temporary value itself. This happens after set_b returns, but before the end of the statement.

I would rewrite the second piece as either

let chainer = Chainer::new().set_b(100);
chainer.print();

or


let chainer = Chainer::new();
let chainer = chainer.set(100);
chainer.print();

Thanks for the responses.

I was a little surprised that this worked.

let chainer = Chainer::new();
let chainer = chainer.set(100);
chainer.print();

Would it make sense if the compiler to do this behind the scenes too? It would save some typing.

And what should be the rule you propose?

In looking at the MIR of the example code and applying some of the desugaring logic in the Rustonomicon, I came up with the following "picture" of what I believe captures what has been collectively described in the above responses.

fn main() {
    a: {
        let chainer;  // the method callS require a "hold that thought :)" 
                      // while Rust first computes the right hand side

        b: { // the chain of method calls uses a series of temp variables
             let temp_1: Chainer = Chainer::new();
             c: {
                  let temp_2: Chainer = Chainer::set_a(mut temp_1, 1);
                  d: { // the last method in the chain, chainer is assigned a value
                       chainer: Chainer = Chainer::set_b(mut temp_2, 2);
             }
        }
        e: { // new scope for short-lived &chainer
             println!("Chainer: {:?}", &'e chainer);
        }
    }

 
    f: {
        // no use of temp variables
        let chainer_b = Chainer::new();
        
        // chainer_b.set_b(100).print();
        g: { // temp variables used once more...
             // chainer_b is moved to temp_3 <<< Key
             let temp_3: Chainer = Chainer::set_b(mut chainer_b, 100);
        
             h: { // the print function executes using the short-lived &'h temp_3 
                Chainer::print(&'h temp_3);
             }
        }
        // error[E0382]: borrow of moved value: `chainer_b`
        println!("Chainer B: {:?}", chainer_b); 
}
1 Like

If the method being called takes self, returns Self, and that return value would otherwise be dropped, assign it a shadow variable with the same name:

let chainer = Chainer::new();
chainer.set_a(100);

should be translated internally to

let chainer = Chainer::new();
let chainer = chainer.set_a(100);

That would also work when the struct implements/derives the Copy/Clone trait.

let chainer = Chainer::new();
chainer.set_a(100);

The above would normally have no effect on chainer, because a copy thereof would've been passed Chainer::set_a(copy, 100). The compiler doesn't give a warning and unsuspecting developers would find themselves with an unmodified chainer.

Were the translation to kick in, chainer would have the expected modifications/mutations.

Yes, thank you! That does indeed make it much clearer what's happening.

If only there were a way to generate that with either rustc. I found rustviz but it's a separate tool and doesn't bind into rustc and implements its own logic, so it would not necessarily recognize the same errors as the compiler.

1 Like

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.