rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.82k stars 1.55k forks source link

`move` `Copy` values #3670

Open crlf0710 opened 3 weeks ago

crlf0710 commented 3 weeks ago

Copy values have special copy semantics, when they're moved away, the old place still have valid value alive. A lot of times this is not people want. For example, it's very easy to typo and wrongly use some temporary values after they have been sent away with assignment.

If possible, i'd propose a move expression:

`move` `(` expr `)`

The parenthesis are for avoiding the ambiguity with move closures.

Example:

let a = 42;
x.a = move(a);
double(&mut x.a);
dbg!(a);  // compile error

Alternatives:

Diggsey commented 3 weeks ago

For example, it's very easy to typo and wrongly use some temporary values after they have been sent away with assignment.

I've never seen this be a problem, since by definition the old value is still valid? Do you have an example of where this can cause a problem?

Assuming there's an example where this is a problem, why would someone remember to use the "move" modifier, if they forgot not to use the moved value? Typically these problems are solved in the type definition rather than the use-site, since it avoids this problem, but this solution already exists - you can just not implement Copy for types which you want to be moved.

kennytm commented 3 weeks ago
macro_rules! unlet {
    ($a:ident) => {
        #[allow(unused_variables)]
        let $a: (/* this variable has been disabled in this scope */);
    }
}
let a = 42;
x.a = a;
unlet!(a); // <-- new
double(&mut x.a);
dbg!(a);  // error[E0381]: used binding `a` isn't initialized

or you could just use { … } to impose the normal scoping rule...

{
    let a = 42;
    x.a = a;
} // <-- restrict scope of `a`
double(&mut x.a);
dbg!(a); // error[E0425]: cannot find value `a` in this scope
crlf0710 commented 3 weeks ago

Yes, this unlet macro is good enough.

I'm still a little curious why @Diggsey has never seen this be a problem. When writing algorithms, and there's more than one pointers or indices or something lightweight but copyable. I feel this is so common. Sorry I don't have code to share at the moment though. These types cannot be defined as non-copyable but this invalidation only happens within specific portion of algorithm.

kennytm commented 3 weeks ago

These types cannot be defined as non-copyable but this invalidation only happens within specific portion of algorithm.

Sorry but this speaks more about the API design of the code than the Rust language. It is trivial to wrap the Copy type in a non-Copy structure such as

#[repr(transparent)]
#[derive(Debug)]
struct NoCopy<T>(T);
impl<T> NoCopy<T> {
    pub fn new(inner: T) -> Self {
        Self(inner)
    }   
    pub fn into_inner(self) -> T {
        self.0
    }
}
    let a = NoCopy::new(42); // <-- wrap the type into NoCopy
    x.a = a.into_inner(); // <-- consume the NoCopy
    double(&mut x.a);
    dbg!(a);  // error[E0382]: use of moved value: `a`

You could further enhance the safety by making the API accept specialized NoCopy-styled wrapper types only, and also making their new() and into_inner() private to the low-level modules.

And even if this RFC is implemented, as @Diggsey said in the 2nd paragraph of their comment, if you forget to annotate a move(a) the compiler will never be able to notice because copying is still valid, so this RFC offered no safety guard against misuse. Meanwhile if the variable a itself is declared NoCopy<i32> instead (i.e. "solved in the type definition rather than the use-site"), Rust can easily statically prevent double-use.