Does moving out is not a writing?

The code below, x.m can be moved out, but it can't be assigned.
The question is:
Doesn't let c = x.m be considered a writing to x?

struct st<'a> {
    n: &'a mut i32,
    m: String,
}

fn b<'z>(x: st<'z>) -> &'z mut i32 {
    let c = x.m;
    x.m = "2".into();
    x.n
}

error[E0594]: cannot assign to `x.m`, as `x` is not declared as mutable
   --> src/main.rs:182:5
    |
182 |     x.m = "2".into();
    |     ^^^ cannot assign
    |
help: consider changing this to be mutable
    |
180 | fn b<'z>(mut x: st<'z>) -> &'z mut i32 {
    |   

No. It reads that field.

I think it's a partial move:

struct st<'a> {
    n: &'a mut i32,
    m: String,
}

fn b<'z>(x: st<'z>) -> &'z mut i32 {
    let c = x.m; // partial move
    let c = x.m; // compiler error here
    //x.m = "2".into();
    x.n
}

(Playground)


Note that a full move doesn't require mut either:

fn main() {
    let x = String::new(); // no `mut` required here
    drop(x); // full move
}

(Playground)

It is partial move, but seems there is some conflicts on concept.

And a read only T can be promoted to writable T:


impl<'a> st<'a> {
    fn a<'z>(mut self) -> &'a mut i32 {
        self.n
    }
}

fn main() {
    let mut nine = 9;
    let n = {
        let s = st {
            n: &mut nine,
            m: "0".into(),
        };

        s.m = "2".into(); // --------- fails
        s.a()  -------------// st ---> mut st is OK
    };

}

The main intention for mutability markers on local variables in Rust, i.e. let mut x = … vs let x = …, but also for function arguments like x vs mut x as in your examples, is to help make code generally more predictable.

As such, even though the variable is considered “immutable”, actually two kinds of kind-of-mutating operations are allowed for an immutable variable: First initialization, and final moving-out. The rationale for this is that before first initialization and after finally moving-out of a variable, the variable is not accessible anyways (it may be in scope, but you’d get a compilation error if reading from the variable before it’s first initialized or after it’s been moved out of), so throughout the whole period where it is accessible, it will not change in value, which is sufficient to get the desired predictability you’d expect from immutable variables: The value is always the same it was when it was defined/initialized.

As some simple demonstration code, you can do

let x: String; // declaration

x = "Foo".to_string(); // initialization

drop(x); // moving out

all to an immutable variable x. Declaration and initialization is often combined, and moving out of the value is sometimes skipped, so it’s just implicitly dropped, and even in cases where it does happen, it commonly happens to the whole value at once. But moving out individual fields, i.e. de-initializing the value step-by-step so to speak, is just as fine, and the rationale is the same.

struct Foo {
    x: String,
    y: String,
}

let v: Foo; // declaration

v = Foo {
    x: "Foox".to_string(),
    y: "Fooy".to_string(),
}; // initialization

drop(v.x); // moving out 1
drop(v.y); // moving out 2

(Initializing in multiple steps is not easily possible as far as I’m aware.)


There’s another interpretation of “writing” / “mutation” in Rust, slightly but significantly different from what mut variables allow you, which is access via &mut … references. To categorize the action of moving out of a field in the spectrum of accesses that references provide (between &T and &mut T), actually, (both first initializing a value and) moving out of a value are operations that are not possible via by-reference access at all. It’s a purely owned-access kind of operation to leave a value in an (true) moved-out-of state.

The workaround when working via references comes in the form that mutable references while not allowing moving out a value, will still allow replacing it, and as such, particularly for types like Option<T> you can achieve what amounts to essentially moving out a value via functions like mem::take or mem::replace, or Option’s take method. In this case however, the value is still valid, initialized, and accessible, so this kind of operation is considered a true mutation, and requires actual mutable access and a mutable variable.


Another exception from normal “mutable vs. immutable variable” rules come from interior mutability types like RefCell and co. I just wanted to mention that, as the rest of this response does brush over this exception, Rust does not help you make code more predictable in the same way it normally does, when these types are involved, so an immutable-marked variable with a type like RefCell<T> can have its (inner) value actually changed.

3 Likes

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.