While attempting to gain a better understanding of ownership, and change of ownership, refer to the following code snippet:
{
let mut str_val = "ABCD".to_string();
let mut str_val1 = "EFGH".to_string();
let mut str_val2 = str_val + &str_val1; // Ownership of 'str_val' is moved here!!!!!!!
println!("new value = {}",str_val); // Generates compile error per compiler message following
}
Can someone explain why ownership changes in the str_val variable in the right-hand side expression? I only expected this if the right-hand was a stand-alone string variable name.
** | let mut str_val2 = str_val + &str_val1;
| ------- value moved here
** |
** | println!("new value = {}",str_val);
| ^^^^^^^ value borrowed here after move
You relevant impl of Add for Stringhere. If you look at the signature of the add method, you'll see that it takes ownership of self
fn add(self, rhs: Rhs) -> Self::Output
Add could be implemented for &str as well[1], but this way the add operation can reuse the storage owned by str_val rather than having to always create a new allocation.
In your case self is str_val and the ownership of that storage is transferred to str_val2. You can of course fix the error in your code by cloning str_val in the add operation
{
let mut str_val = "ABCD".to_string();
let mut str_val1 = "EFGH".to_string();
let mut str_val2 = str_val.clone() + &str_val1; // Ownership of 'str_val' is moved here!!!!!!!!!!!!!!!
println!("new value = {}", str_val); // Generates compile error per compiler message following
}
Thanks for pointing out why the code behaves in this manner - I totally overlooked that the overloaded '+' would do this.
In this specific case the string addition operator's overloaded run-time behavior results IMO in a non-obvious change of ownership of the right hand side's left variable from the expression. The good thing is that the documentation explains it.
I'm curious if this might be considered one of those cases of unintended complexity which Niko Matsakis alludes to in his youtube "Rust in 2024"?
Note that +always moves/copies its arguments. (So do all the other “overloadable” operators that “combine” things, + - * / | & ^ << >>, but the ones that “compare” them == != < > <= >= take references implicitly.)
The trait implementations that exist for std::ops::Add only affect whether a certain pair of argument types is allowed, not whether a move will occur.
Hard no. Ownership is one of the cornerstones of the language. This particular implementation of string addition makes repeated concatenation (s1 + s2 + s3 + …) take linear time rather than quadratic – it's absolutely intentional.
What could be considered non-obvious in something that is the direct consequence of the signature of a method?
Thanks for the feedback. Apparently my comments should have been clearer, as I didn't intend to suggest that ownership and its role in automatic runtime memory management and safety was misguided.
To be clear, over the past 12+ months I've studied/read several highly regarded Rust books (including 'the Rust book'), watched many Rust Youtube videos, and various blogs, but I had not previously become aware of this behavior of certain in-line operators 'reusing' their sub-expression variable memory for what (how that I'm aware) seems to be a beneficial optimization.
My take is that the community would be well served by expressly publicizing this interesting optimization 'feature' in the major tutorial literature and documentation as it relates to scenarios where ownership is 'Moved'. In the current Rust literature my interpretation is that it's generally stated that moves occur during an assignment statement, or when parameters are passed by value (for 'Move' value types of course). This seems to be a third mechanism for it to occur. Maybe there are others?