4 ms·
I haven't used rust, so can you explain this to me: If I do the rust equivalent of: def add1(x): return x + 1 x = 1 y = add1(x) z =
by dbieber 7y ago
I haven't used rust, so can you explain this to me:
If I do the rust equivalent of:
def add1(x):
return x + 1
x = 1
y = add1(x)
z = add1(x)
then will x have been deallocated by the first call to add1 and will the second call to add1 fail?
[You can ignore the fact that I'm using numbers and substitute an object if that makes more sense in the context of allocating / deallocating memory in rust.]
- saagarjha 7y agoIt depends: if add1 borrows the value then this code is fine. If add1 takes ownership of the parameter then no, this will not work.
- AlanSE 7y agoWhat do you mean by "will not work"? Will it not compile? And how do we write that with add1 taking ownership vs. not?
- db48x 7y agoIt won't compile, because the compiler keeps track of where you moved the value and notices that x now has no value. If you want the function to borrow the argument instead, make it take a reference, &x.
- Filligree 7y agoYou'll get a compile-time error explaining the situation, yes, including documentation links and possible fixes. The differences between the versions are minimal. fn add1(x: Thing) -> Thing { x + 1 } fn add2(x: &Thing) -> Thing { x + 2 } This works on the assumption that whatever "+ 1" really means doesn't itself need ownership of x, e.g. because it mutates it. If it does, then you'll get an error. Or you could do this: fn add3(x: &mut Thing) { x.add(3) } Which, presumably, modifies x instead of returning a copy. It's your job to make sure it makes sense to do that, but Rust has another trick in its pockets: let y = add1(x); // Works; takes ownership. let z = add2(&y); // Works, and doesn't take ownership. let zz = add2(&y); // So you can do it twice. (Y tho?) let h = add3(&y); // Compile-time error! let hh = add3(&mut y); // Works. You need to specify that you're fine with y being mutated.
- saagarjha 7y agoConsider the following code: fn add1(x: Vec<i32>) -> Vec<i32> { // Takes ownership return x.iter().map(|&x| x + 1).collect::<Vec<_>>(); } fn add1_borrow(x: &Vec<i32>) -> Vec<i32> { return x.iter().map(|&x| x + 1).collect::<Vec<_>>(); } The first will take ownership of the thing you pass in, so you can't do something like this: let x = vec![1, 2, 3]; let y = add1(x); let z = add1(x); // error[E0382]: use of moved value: `x` but the follow code is OK, because the function just borrows the value instead of owning (and destroying) it: let x = vec![1, 2, 3]; let y = add1_borrow(&x); let z = add1_borrow(&x); This is legal too: let x = vec![1, 2, 3]; let y = add1_borrow(&x); let z = add1(x); but as before you can't use x after this. (Note, I chose Vec here because Vec does not implement Copy: if I used i32 it would just copy the value in the non-borrowing case, which would make the code fine as no ownership would be transferred.)
- Twisol 7y agoIn Rust, you can tell whether something is borrowed from the call site, without having to look at the function being called. If a function takes a reference (`&T`), then the caller must pass a reference. There's no implicit by-referencing as in C++'s `&` types. (I realize you, saagarjha, probably know this already -- this is more a note to other readers.)
- tridentlead 7y agoNormally yes, if using an object. In this case, the integer types implement the Copy trait, so instead of actually having your first call to add1 take ownership of x, it will just operate on a copy of the value, so your second call will work too.
- saghm 7y agoInterestingly, the type of `x` actually does matter here in Rust! For most types, yes, passing something by value into a function will cause the memory to be "moved", which means that reusing `x` will be a compiler error. That being said, you can also either pass a shared reference (i.e. `&x`), which will allow you to access the data in Rust (provided you don't move anything out from it or mutate it, which would cause a compiler error) or a mutable reference (i.e. `&mut x`), which will allow you to access or mutate the data in `x` but not take ownership of it (unless it's replaced with something else of the same type). However, a few types, including integers, but also things like booleans and chars, implement a trait (which for the purposes of this discussion is like an interface, if you're not familiar with traits) called Copy that means that they should be implicitly copied rather than moved. This means that in the specific example you gave above, there would not be any error, since `x` would be copied implicitly. You can also implement Copy on your own types, but this is generally only supposed to be done on things that are relatively small due to the performance overheard of large copies. Instead, for larger types, you can implement Clone, which gives a `.clone` method that lets you explicitly copy the type while still having moves rather than copies be the default. Notably, the Copy trait can only be implemented on types that already implement Clone, so anything that is implicitly copied be can also be explicitly copied as well
- tux1968 7y agoHow does this implicit Copy trait interact with the Drop function? Doesn't that mean that the implicit copy would be dropped rather than the actual value for those types?
- nemetroid 7y agoIf add1 takes ownership of the argument, yes (and x is not implicitly copyable). Compare with C++, in particular types with deleted copy operators (e.g. unique_ptr<T>). In order to call a function that takes an unique_ptr by value as argument, you must explicitly move the object into the function: void foo(unique_ptr x) { ... } unique_ptr x = ...; foo(move(x)); foo(move(x)); Linters (i.e. clang-tidy) can be configured to complain about this, but it's completely valid C++ (because move leaves the object in an unspecified but valid state). In Rust, the argument will be automatically moved in the first call, and the second call will generate a compile-time error.
- masklinn 7y ago> because move leaves the object in an unspecified but valid state Also and importantly because move() only indicates that the value may be moved from (it's a fancy cast), that doesn't mean it will be moved from. The called function needs to take an rvalue reference (T&&) for a move to possibly happen. That is if you had a void foo(unique_ptr&& x) it may or may not move. Likewise you can call move() on things which are not move constructible (or passing the result to functions which don't care) with no ill effects (save to the reader). IIRC what happens here is that since foo() takes a unique_ptr by value the compiler will insert the construction of a temporary unique_ptr and overload resolution will select the move constructor (since we've provided an rvalue reference, and assuming the wrapped type is move-constructible). > move leaves the object in an unspecified but valid state Also notes that this is the general semantics, but specific types can define their exact behaviour here. In particular, unique_ptr specifies that a moved-from unique_ptr is "nulled" (equal to nullptr).
- cft 7y agoFail as in it won't compile, not as a runtime error