Why does move occur in simple math `+`?

Here's a generic function that makes a vector of sums. It works when I write T: Copy, U: Copy, but does not this way, because it can't move val1, val2 from behind a shared reference.

So, question 1: do I understand this right: when you do even a simple math calculation, you must either borrow & copy, or move the value? (question 2: And for numeric types copy happens silently under the hood?)

use std::ops::Add;

fn add<T, U>(v1: Vec<T>, v2: Vec<U>) -> Vec<T>
    where   T: From<U> + Add<T> + Add<Output = T>
{
	let sum_func = |x: (&T, &U)| -> T {
		let (val1, val2) = x;
		(*val1.clone()) + T::from(*val2.clone())
	};
	v1.iter().zip(v2.iter()).map(sum_func).collect::<Vec<T>>()
}

fn main() {
	let vec1:Vec<i32> = vec![1, 2, 5, 5, 12, 20];
	let vec2:Vec<f64> = vec![1.5, 2.5, 5.5, 5.5, 12.5, 20.5];
	
    println!("{:?}", add(vec2, vec1));
}

question 3: I see Copy trait implements Clone. For numeric calculations, is it ok performance-wise to just clone val1/val2, or it's better to require Copy?

    where   T: Clone + From<U> + Add<T> + Add<Output = T>,
            U: Clone
...
		val1.clone() + T::from(val2.clone())
...

It depends on the Add implementations available. It's technically possible (but discouraged in case of non-trivial values) to have an implementation which goes from &T + &U to V directly.

Essentially, yes - implementations for binary operations on references to primitive numbers delegates to the implementation for the numbers themselves by dereferencing.

If the value is Copy, it's guaranteed that it can be memcpyd without problems. In that case, Clone is expected (not obliged, but expected) to behave exactly like Copy.
If the value isn't Copy, then cloning it can have arbitrary effects like memory allocation.

So, it's up to you whether you can tolerate these effects in your code or not. But for Copy types, there should be no difference (and the exceptions are probably some degenerate cases).

1 Like

Answer 1:
Yes, you can't move an object out of a shared reference, because that would mean taking ownership of it and rust only has a single owner for all data.

Answer 2:
For all types implementing copy it will be used when moved out of a shared reference or when passing as a value to a function.

Answer 3:
I think Clone::clone() is generally slower than copying you data. But I'm not 100% sure.

Bonus tipp:
You already own the vectors in the add function. Then you can simply use

v1.into_iter().zip(v2.into_iter()).map(sum_func).collect()
1 Like

Thanks.

Oh, that's a mistake in my example. The vectors should be borrowed and not destroyed in the process.

No. For Copy types, any sensible implementation of Clone also only performs a bitwise copy.

2 Likes

Can you explain why this is discouraged?

Maybe this has been mentioned in or is related to: Surprising: &1 + &1 == 2

But I don't remember.

Simply because it's highly surprising when the arithmetic operation incurs, e.g., some hidden .clone().

1 Like

Wouldn't it be better to have an impl for &T + &U -> V that allocates, than to require ownership and thus require cloning an input whose storage cannot necessarily be reused? Or am I misunderstanding the alternative proposed?

1 Like

The best way in many cases probably is to do as it is done with String, i.e. to have T + &U -> V - in this case, you can reuse the allocation from the left side, but not force the user to give up ownership of the right side.

3 Likes

If you're going to do that, it's probably best to implement AddAssign and then dispatch to it in a generic Add implementation, so that they always stay in sync:

(untested)

impl<U> Add<U> for MyType where Self: AddAssign<U> {
    type Output=Self;
    fn add(mut self, rhs:U)->Self { self += rhs; self }
}
2 Likes

You can only reuse the allocation from the left side in some cases (if there is enough spare space). So a.clone() + &b may do two allocations, whereas &a + &b would only do one allocation. So &a + &b is better.

It's best to have all 4 versions.

2 Likes