Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is assignment to the data member of a temporary allowed?

Tags:

rust

Why is the assignment in the following snippet allowed?

#[derive(Copy, Clone)]
pub struct X {
    pub a: u8,
}

fn main() {
    let x = X { a: 0 };
    { x }.a = 5;
    assert!(x.a == 5); // I was wrong
}

Am I assigning to a temporary?

Although these are forbidden:

({ x.a }) = 5;
let mut y: u8 = 0;
({ y }) = 5;

I ask, because I had a bug like

unsafe { *some_struct }.mem1 = 5;

I didn't use

unsafe { *some_struct.mem1 = 5; }

from the beginning, because it felt right to keep unsafe blocks as short as possible.

like image 979
nicolai Avatar asked Oct 20 '25 08:10

nicolai


2 Answers

Yes, you are assigning to a temporary, because X implement Copy, so doing { x } you are creating a block that return x, but X implement Copy, so your block return a copy of x. Your block is basicly this:

#[derive(Copy, Clone)]
pub struct X {
    pub a: u8,
}

fn main() {
    let x = X { a: 0 };
    let mut tmp = { x };
    tmp.a = 5;
    assert_eq(x.a, 0);
    assert_eq!(tmp.a, 5);
}

What you can do to make the block return the actual variable instead of the copy is to transform the pointer to a mutable reference, and return that ref from the unsafe block:

#[derive(Copy, Clone)]
pub struct X {
    pub a: u8,
}

fn main() {
    let mut x = X { a: 0 };
    let ptr: *mut X = &mut x;

    unsafe {
        &mut *ptr
    }.a = 5;
    
    assert_eq!(x.a, 5);
}

Why is it allowed? It is allowed because Rust lets you modify temporaries, because a lot of things would be very verbose otherwise, like:

fn get_first<T>(vec: Vec<T>) -> Option<T> {
  let first = vec.into_iter().next();
  //                         ^ temporary iterator
  first
}

next() needs a mutable ref to the iterator. Having to bind it to a mutable variable would mean that you would have to write:

fn get_first<T>(vec: Vec<T>) -> Option<T> {
  let mut iter = vec.into_iter();
  let first = iter.next();
  first
}

The snippets of forbidden code that you provided don't satisfy the compiler because you try to assign a value to another value, not to a variable: doing { var } creates a block that return the value in the variable, so:

({ x.a }) = 5;
//       ^ block return the value in x.a
let mut y: u8 = 0;
({ y }) = 5;
//     ^ same here, return the value in y

You can consume this temporary, but not overwrite it, this is intentional and a good thing. I think in C++ they call this lvalue and rvalue, the block return a rvalue, and you can't assign to a rvalue.

But yes the compiler don't see { x }.a as a temporary, which is a bit silly, there is no use case that I can think of, and can create confusion.

like image 61
Bamontan Avatar answered Oct 22 '25 04:10

Bamontan


I tried to simplify as much as possible the initial example. No more Copy or Clone are involved; only temporaries are explicitly created.

The first attempt with 1 and 11 is quite normal; nothing more to be said.

The second attempt with 2 is totally useless since the created temporary disappears right after its creation (no side effect, this will probably be optimised out).

The third attempt with 3 and 33 is even more useless than the previous one since we alter the temporary just before it disappears (no more side effect, this will probably be optimised out).

The fourth attempt with 4 and 44 directly relates to the question. Although it is as useless as the two previous attempts, it is rejected by the compiler. The error code is E0070 and rustc --explain E0070 says:

The left-hand side of an assignment operator must be a place expression. A place expression represents a memory location and can be a variable (with optional namespacing), a dereference, an indexing expression or a field reference.

The section Place Expressions and Value Expressions of The Rust Reference says something similar with more details.

Thus, the raw answer about the difference between the third and fourth attempts simply stands in the fact that the field reference of the third attempt is considered as a place expression.

However, I don't find this answer very satisfying since I don't know the motivation of the language designers in this choice. Why do they consider a temporary as a value expression but consider a field of this temporary as a place expression?

struct X {
    a: u8,
}
fn main() {
    let mut _x = X { a: 1 };
    _x = X { a: 11 };

    X { a: 2 }; // created, then dropped straight away

    X { a: 3 }.a = 33; // created, altered, then dropped straight away

    // X { a: 4 } = X { a: 44 }; // fails with error E0070
}
like image 43
prog-fh Avatar answered Oct 22 '25 04:10

prog-fh