Confused about "this reinitialization might get skipped" error

I have ran into an issue developing an application in Rust which I can't figure out. I am new to Rust and low level languages in general, so bear with me if this a newbie question.

The code where I get this error message is quite large, but I've managed to create a small snippet which even if it doesn't make sense in what's trying to do, it does reproduce the behavior I'm interested in.

enum Item {
    Type1 {
        optstrfield: Option<String>,
        strfield: String,
    },
}

struct Owner {
    pub items: std::collections::LinkedList<Item>,
}

fn main() {
    let mut owner = Owner {
        items: std::collections::LinkedList::new(),
    };
    owner.items.push_back(Item::Type1 {
        optstrfield: Some("text".to_string()),
        strfield: "text".to_string(),
    });
    let owner_ref = &mut owner;
    for item in &mut owner_ref.items {
        match item {
            Item::Type1 { optstrfield, strfield } => {
                // This if block works
                if let Some(s) = optstrfield {
                    println!(
                        "{} {}",
                        format!("{}", s),
                        format!("{}", strfield),
                    );
                } else {
                    println!(
                        "{} {}",
                        format!("{}", strfield),
                        format!("{}", strfield),
                    );
                }
                // But the following equivalent statement doesn't
                // println!(
                //     "{} {}",
                //     format!("{}", if let Some(s) = optstrfield { s } else { strfield }),
                //     format!("{}", strfield),
                // );
            }
        };
    }
}

I make two different (but as I see them, equivalent) uses of the matched variables in the only match arm, but only the first one compiles. The second one (commented out) makes the compiler complain about "this reinitialization might get skipped" and trying to borrow moved values.

What exactly is the difference between those two options that makes one valid but not the other?

I'm not sure what "this reinitialization might get skipped" is supposed to mean, but the error itself is a simple violation of the borrow checker, and the compiler tells it:

move occurs because `strfield` has type `&mut String`, which does not implement the `Copy` trait

42 | format!("{}", if let Some(s) = optstrfield { s } else { strfield }),
   |                                                         -------- value moved here
43 | format!("{}", strfield),
   |               ^^^^^^^^ value borrowed here after move

strfield has type &mut String which cannot be copied or cloned, because only a single mutable reference to the specific data can exist at each time. Simply using a variable, like you did with strfield in the else branch, means that you move the value to a new location. In this case from strfiield into the return value of the if-let expression, which is then borrowed by the format! macro. So once you move strfield, you can no longer use it in the next format!("{}", strfield) call.

But you don't really need to move strfield in that if-let expression, since the value will be immutably borrowed anyway. If you could just coerce a &mut String into a &String, you could freely copy it without invalidating strfield. And you can do that, with a reborrow!

A reborrow means that you simply create a new reference from an old one directly via borrowing, i.e. write &*strfield. This makes the old reference inaccessible for as long as the new reference lives, but once it dies you can freely use the old reference. Now, format! is a macro, so the lifetimes of its arguments are its implementation detail, but in fact it holds a reference only long enough to append it to the string, and drops it before processing the rest of the arguments. This means that your code can be fixed with this simple reborrow.

You can also reborrow a mutable reference as a new mutable reference: let s = &mut *strfield, with similar semantics. strfield is inaccessible as long as s lives, but once s dies you can use strfield again. This is, in fact, what happens with &mut T function arguments. For example, consider this code:

struct T;

fn use_mut(_: &mut T) {}

fn use_many_muts(x: &mut T) {
    use_mut(x);
    use_mut(x);
    use_mut(x);
}

Each use_mut(x) should move x into use_mut function, which would mean that x is dead after the first use_mut call. But this code compiles fine, how? The answer is implicit reborrowing. If the function parameter has explicitly the form &mut T, then the compiler inserts a &mut* reborrow for that parameter. I.e. the code above really has the form

fn use_many_muts(x: &mut T) {
    use_mut(&mut *x);
    use_mut(&mut *x);
    use_mut(&mut *x);
}

The reborrows are moved into the function as usual, and die once the function returns, making x accessible again.

Note that the implicit reborrow applies only to parameters which explicitly have the form &mut T for some T. It does not happen when you pass a &mut T into a generic parameter. I.e. the following fails to compile:

struct T;

fn use_generic<X>(_: X) {}

fn use_many_muts(x: &mut T) {
    use_generic(x);
    use_generic(x);
             // ^ use of moved value: `x`
    use_generic(x);
             // ^ use of moved value: `x`
}
4 Likes

means that you move the value to a new location. In this case from strfiield into the return value of the if-let expression

Thanks, this part here clarified the whole thing. It wasn't obvious to me that variables were moved just by returning them from a block. For example, look at the following examples, where the first one compiles but the second one doesn't:

let s = "hello".to_string();
println!("{}", s);
println!("{}", s);
let s = "hello".to_string();
println!("{}", { s });
println!("{}", s);

I guess it's a rough edge that may eventually be fixed.

I also really appreciate the explanation of reborrows, which I didn't know. I will surely make use of them from now on. Thank you!

The move can be semantically relevant in other ways. Even if that doesn't preclude a change, it would be inconsistent with how function blocks must work, and destructor scopes more generally.

2 Likes

I don’t think that there’s anything that could (or should) be “fixed” about the general behavior that returning a value from a block results in a move. That’s fairly common knowledge in Rust, and lots of code out there will actually rely on it and use { x } instead of x in certain situation where you want to move the value. (It’s not super common that you need this, but common enough that there’ll be a significant amount of existing code relying on this behavior.)

What can be fixed though in my personal opinion (and this would happen to at least make your concrete code example here compile after all) is moving mutable references. I’m convinced that it should be possible to eventually make it so that every move of a mutable reference will instead only reborrow it.

7 Likes

Additionally, all format macros internally take references of everything you're passing in - so passing a value or &Mut multiple times is fine, because the format machinery gets & &mut String, which we can clearly have multiple of.

1 Like

I see. I wasn't aware of this and it looked weird at first sight. Thanks for the clarification, still a lot to learn I guess!

What else do you think should or could happen? How do you propose by-value function returns work if not by moving?