Why does the add
method on strings take ownership of the first string being added? Wouldn't it be possible to just take in a mutable reference to the first string instead??
Operator's come from Traits. (Add) There is only one definition that all implementations must follow.
It would, but an arithmetic operator (that is expected to be "pure") mutating its arguments would be surprising. If you want to mutate the left hand side, then use +=
.
Furthermore, taking ownership of the LHS makes it possibe to reuse its allocation, avoiding a copy, thus repeated concatenation using +
doesn't suffer from the Shlemiel problem (unlike basically every other language).
There is a version that takes a &mut String
: addAssign
. add
corresponds to the +
operator, addAssign
corresponds to the +=
operator. These 4 statements do the same exact thing:
a = a.add(b);
a = a + b;
a.addAssign(b);
a += b;
Not quite correct. First of all, +=
may be implemented differently than what +
does. Moreover, if you write a = a + b
and if the type of a
isn't Copy
, then you temporarily move out of a
which means in case of a panic during execution of std::ops::Add::add
, the value a
would be set to no value. This leads to the compiler refusing this operation in some contexts:
#[derive(Debug)]
struct Person {
name: String,
height: u32,
}
fn foo(person: &mut Person) {
person.height = person.height + 5; // this works
//person.name = person.name + " Doe"; // but this doesn't compile
person.name += " Doe"; // even though it does the same as this
}
fn main() {
let mut p = Person {
name: "John".to_string(),
height: 100,
};
foo(&mut p);
println!("{p:?}");
}
In the example above, person.height = person.height + 5;
works fine. But person.name = person.name + " Doe";
does not work because if that operation would panic, then person.name
would not be valid anymore.
Sometimes this isn't a problem, like when you do the operation direction in main
, because a panic would cause p
to be dropped.
#[derive(Debug)]
struct Person {
name: String,
height: u32,
}
// Try to uncomment this:
//impl Drop for Person {
// fn drop(&mut self) {
// }
//}
fn main() {
let mut p = Person {
name: "John".to_string(),
height: 100,
};
p.height = p.height + 5;
p.name = p.name + " Doe"; // in this context, the line works, unless `Person` implements `Drop`
p.name += " Doe";
println!("{p:?}");
}
However, that only works if Person
doesn't have a drop handler, because if it had, then p.name
might be invalid when the drop handler runs.
P.S.: Regarding consuming a value and returning a new one versus working on &mut
references, I found this post by @steffahn very enlightening (which is why I came up with the above examples in regard to panic behavior and partial moves).
Sorry for my complex post. Regarding the original question, it's the +=
operator (through the std::ops::AddAssign
trait), which @tczajka and @H2CO3 mentioned in their posts, that does exactly what you propose. The corresponding trait (with &str
as right hand side) is implemented on String
s and the operator then takes a &mut String
on the left side, and a &'a str
for any lifetime 'a
on the right side.
Minimum example:
fn main() {
let mut x = "Hello".to_string();
x += " World!";
println!("{x}");
}
Output:
Hello World!
Actually I always used String::push_str
instead, as I didn't know that +=
is implemented on String
s. So that's something new I learned today.
Maybe it could be, but it's not. All 4 versions end up delegating to push_str
.
In case of String
, they are implemented consistently, but not generally.
But even in case of String
they behave differently in regard to whether the compiler accepts them in certain contexts due to the partial move (as demonstrated in the Playground examples). I.e. it can matter which variant you use, even in case of String
s.
Now that I'm thinking about it again, I believe a = a + b;
(written exactly like that, and where a
is a String
, and b
is a &str
) never can cause a problem? Unless a
is actually something like p.a
. But not totally sure.
@H2CO3 Oh thank you for that article. It was definitely an interesting one that I hadn't thought about before.
@tczajka Thank you for this I just wanted to confirm that my conceptual understanding was correct in thinking that a mutable reference could be used.
@jbe Disclaimer: I have not learnt about traits and handling panics yet (I'm still going to through the Rust book) so some of these questions may/will be from ignorance. Why doesn't person
get dropped in the first case and only in the second case does it get dropped in case of a panic?
In all cases that I brought up, Person
gets dropped eventually.
However, when a String
is stored directly in a variable a
and you write a = a + "something"
and a panic happens during the concatenation (e.g. because of a memory allocation failure), then the stack will be unwound, such that it's not possible to access a
anymore (because it will not exist anymore when the stack is unwound).
Thus, I believe, writing a = a + "something"
is always okay if a
is of type String
.
However, when you have a different value that contains a String
, e.g. where p.a
is the String
and p
is a mutable reference to that value which contains the String
, then you can (edit: would) run into problems when you write p.a = p.a + "something"
(edit: assuming the compiler wouldn't prohibit you from doing that), because when a panic happens here, then the value referenced by p
(Person
in my examples) still exists. But it's not clear what p.a
is set to then, because p.a = p.a + "something"
will temporarily take the String
out of p
, concatenate it, and then put it back. If a panic happens, it's not put back and p
would be an invalid value (because what value will p.a
be set to?).
Thus my warning that someString =
someString + "something"
can sometimes cause problems, while someString += "something"
does not cause problems.
Sometimes the compiler can deduce that in case of a panic p
is dropped before anyone can access p.a
(which might be invalid), see (this previous Playground example, which works unless you comment out the Drop
implementation).
However, Person
may have a so-called drop handler (see step 1 in this list in the reference). Then, when a Person
gets dropped, the destructor will run Drop::drop
(not to be confused with the function std::mem::drop
, which is related but something else). The destructor gets access to the Person
. Thus if p.a
would not be a valid value anymore, the program would run into undefined behavior.
I would conclude that:
- when
a: String
, thena = a + "something";
is okay, - when
a: String
andp: Person
, thenp.a = p.a + "something";
is okay as long asPerson
does not implement a drop handler, - when
a: String
andp: &mut Person
, thenp.a = p.a + "something";
does not compile.
Not entirely sure if I'm right here though.
Bottom-line is: There is a difference between writing something =
something +
otherthing and something +=
otherthing (even when something is of type String
, where the +
and +=
operators are implemented consistently). That is because writing something =
something +
otherthing will temporarily move out of something before putting it back while something +=
otherthing will pass a mutable reference to something to the function which implements the operation.
If you want to extend a String
, I would probably always use the +=
operator or the String::push_str
method instead of writing something =
something +
otherthing.
P.S.: My apologies if my explanations are a bit complex. This stuff certainly requires a bit of deeper knowledge about Rust in regard to how clean-up is performed when values get dropped. Feel free to ask if something is not clear, my explanation is confusing and/or I missed to explain something.
No, it can't cause any problems. Rust tracks the initialization state of every value, and when it's not possible to infer statically, it will insert drop flags. If a struct becomes dynamically partially uninitialized, drop flags will be set accordingly, and there will be no use-after-free or double-free.
Here, person.name
is of type String
, right? And I write person.name = person.name + " Doe";
which fails to compile:
#[derive(Debug)]
struct Person {
name: String,
height: u32,
}
fn foo(person: &mut Person) {
person.height = person.height + 5; // this works
person.name = person.name + " Doe"; // but this doesn't compile
person.name += " Doe"; // even though it does the same as this
}
fn main() {
let mut p = Person {
name: "John".to_string(),
height: 100,
};
foo(&mut p);
println!("{p:?}");
}
Errors:
Compiling playground v0.0.1 (/playground)
error[E0507]: cannot move out of `person.name` which is behind a mutable reference
--> src/main.rs:9:19
|
9 | person.name = person.name + " Doe"; // but this doesn't compile
| ^^^^^^^^^^^---------
| |
| `person.name` moved due to usage in operator
| move occurs because `person.name` has type `String`, which does not implement the `Copy` trait
|
note: calling this operator moves the left-hand side
--> src/main.rs:9:19
|
9 | person.name = person.name + " Doe"; // but this doesn't compile
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0507`.
error: could not compile `playground` due to previous error
That's what I meant with "problems".
I didn't mean there will be problems at runtime. I just wanted to say that the compiler will complain such that bad things at runtime can not happen. Maybe I didn't express that clearly.
I updated my post to emphasize that it would only be a problem if the compiler didn't stop you from doing it.
This brings me to the following question:
struct Person {
name: String,
}
impl Drop for Person {
fn drop(&mut self) {
// could `self.name` be set to "John Do" instead of "John" or "John Doe" here?
// (assuming that memory allocation had failed)
assert!(self.name == "John" || self.name == "John Doe"); // i.e. may this fail in theory?
}
}
fn main() {
let mut p = Person { name: "John".to_string() };
p.name += " Doe";
}
But that's not because it's in a struct. It's because the string is behind a reference. Your example would fail to compile without a struct just as well: *ref_to_str = *ref_to_str + "foo"
doesn't compile, either.
You are never allowed to move out of a reference, regardless of whether the referent is or isn't a struct. This is not because of panicking, either; if you moved out of a reference, its referent would immediately become invalid (even if temporarily), but that's instant undefined behavior, since references must always point to valid values. This holds for all types and values, not only struct fields.
In contrast, doing the same by-value would work, whether or not the string is a struct field. s.field = s.field + "foo"
compiles and behaves as expected if s
is a struct (and not a reference).
Okay, I see. So it's about being a (mutable) reference and not about being a struct.
This matches what I guessed here:
But it compiles only if that struct doesn't implement Drop
:
#[derive(Debug)]
struct Person {
name: String,
}
impl Drop for Person {
fn drop(&mut self) {}
}
fn main() {
let mut p = Person { name: "John".to_string() };
p.name = p.name + " Doe"; // `p` is not a reference, but this line fails
p.name += " Doe";
println!("{p:?}");
}
Errors:
Compiling playground v0.0.1 (/playground)
error[E0509]: cannot move out of type `Person`, which implements the `Drop` trait
--> src/main.rs:12:14
|
12 | p.name = p.name + " Doe"; // `p` is not a reference, but this line fails
| ^^^^^^
| |
| cannot move out of here
| move occurs because `p.name` has type `String`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0509`.
error: could not compile `playground` due to previous error
Right, destructuring a Drop
type is not allowed, as it doesn't make sense. (Should the destructor run then? If so, partial dropping is not possible. If not, that's misleading.)
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.