Reputation: 32374
I'd like to determine in which general (or specific) situations one would not want to use a reference to pass an object into a function in Rust.
I'm mostly interested in functionality (e.g. a function is made so that it cleans up a resource moved into it), but performance reasons would also be interesting (e.g. if it was the case that it's faster to copy a small structure than access it through a pointer).
This regards non-Copy
types, of course.
Note: it's clear why move semantics are used in assignments, in order to prevent aliasing e.g., but in functions that would not be a problem.
Upvotes: 4
Views: 491
Reputation: 60207
Consider a conversion function fn(T) -> U
. If T
was taken by reference, all operations on U
would be required to uphold the invariants in T
. This means that one would not be allowed to destroy T
in any way. Further, the caller would be responsible for keeping the input in place in order for U
to remain valid. A good example is Vec::into_boxed_slice
.
Another example is a function that moves its input. Evidently this cannot accept a &mut
without worrisome semantics. An example would be Vec::insert
.
Another option is when &mut Trait: Trait
. In this case taking T: Trait
allows the caller to decide whether to borrow with dynamic dispatch or pass arguments, which has ramifications for ease of use (in both directions), speed and code bloat.
Another example might just be the preference of convenient syntax for the common case, where cloning is cheap. An example could be Index<Range<usize>>
for Vec
.
There are also compelling safety reasons if you want to restrict the number of times a function can be called on a given object. The simplest example is drop
, which you rightly can only call once. A very fancy example are "session types" for channel communication.
Upvotes: 2
Reputation: 23586
There are some situations like that, all of them are related to lifetime of structure, i.e. I'm writing Octavo (crypto library) where we have some hash functions (i.e. SHA1):
let mut digest = SHA1::new();
digest.update("Ala ma kota!");
let result = digest.result(); // here we invalidate digest as it finished its work
Other usage for case like above is builder pattern:
PizzaBuilder::new()
.add_sauce()
.add_cheese()
.add_topping(Topping::Tomato)
.add_topping(Topping::Bacon)
.add_topping(Topping::Jalapenio)
.bake() // here we destroy PizzaBuilder as this setup isn't usable anymore
// you should create new PizzaBuilder for another pizza
// (you cannot reuse dough once you bake pizza)
Upvotes: 2
Reputation: 128101
One of applications of move semantics is type-safe state machines. Suppose that you have an object which can be in two states, uninitialized and initialized, and going from uninitialized to initialized has side effects and can possibly fail. This is naturally modeled with two types with a transition method which accepts an object of the first type by value and returns an object of the second type:
pub struct UninitFoo {
...
}
impl UninitFoo {
pub fn new() -> UninitFoo { ... }
pub fn configure_something(&mut self) { ... }
pub fn configure_something else(&mut self) { ... }
pub fn initialize(self) -> Result<InitFoo, SomeError> { ... }
}
pub struct InitFoo {
...
}
impl InitFoo {
pub fn do_some_work(&mut self) { ... }
}
The example above is really contrived but I guess you get the idea. This way your set of types essentially forms a state machine, where methods like initialize()
are transitions between states.
Upvotes: 0