I might get dinged on the strict technicalities of what I'm about to describe. Notwithstanding, here is my take on the question of whether it's good practice, or a good starting point in the design, to have your functions take ownership of the parameters.
taking ownership ~ "pass by value" with a Rust twist
My mental model is that taking ownership is equivalent to "passing by value". This is where a copy* of the value is made during the "hand-off" from the caller's scope to that of the function (where it is consumed in the body of the function). So once the body of the function falls out of scope, unless the function returns it, the value is dropped.
There are other "side-effects" of moving ownership over to the function that are unique to Rust**. Once the caller passes the value to such a function, the caller can no longer use it; the value was "moved" out of the caller's scope. Another view of this is that you have given the function "all rights" to that data (read/write and consume). However, this transfer of "rights" can be made temporary by having your function return the value to the caller: fn tmp_ownership(input: MyData) -> MyData
.
borrow ~ read-access
The model of "rights" is useful because it focuses on the ultimate utility, the motivation, and less on "performance" and optimization which I only consider later in the design. With that said, when your function only needs read access to the data, pass it a non-exclusive borrow reference: &MyData
. I think it's a given that we all understand why it's inherently cheaper and easier to manage "read-only" access to the data (perhaps name the required lifetime 'data
).
exclusive borrow only when taking ownership is not an option
The last choice is my least preferred, but sometimes "capitulate" based on convention or usability: the "exclusive borrow". The exclusivity (only one) gives you the right to mutate the underlying data. This is my least preferred not because I don't believe in mutation, but more because I can get mutation by transferring ownership, the first option. I like transferring ownership as a "first goto" for mutation because it comes with a built-in accounting system that requires I make my intention explicit: give ownership, mutate, return ownership. It avoids the less explicit "side-effect" of fn foo(input: &mut MyData) -> ()
. The implied returned value of ()
is my point: it looks like the function doesn't do anything, only "behind the scenes", it mutates your data. I don't want to overstate this point, but I make it only to provide a way to prioritize what you need, with how to implement it without introducing more complexity than is required; having &mut
references in code requires extra attention.
a copy event always happens when calling a function, so what are we actually talking about
* I used the word "copy". I'm using it for what it means "in english", but there is an important contrast to Copy
in Rust. Whatever you put in the parameter of the function is copied, so the use of that word is correct. The question is what is copied: a borrow ref, an exclusive borrow ref, or the value itself. They all involve a bit-wise copy and transfer of ownership - it's just "of what" And whether the memory of the original remains "valid" (see below). For instance, "I own the borrow ref" before and after I used it to call the function. From a performance perspective, the only concern might be copying the value itself. The good news is that bit-wise copying is generally cheap and thus is only be a secondary, downstream optimization concern.
optional toggle to a more intuitive "pass by value" behavior
** You can "opt-out" of the default "move" semantic for MyData
by having it implement Copy
; doing so signals to the compiler to use a "copy" semantic - resulting in two independent, bitwise copies. This latter behavior is more the "pass by value" behavior you might expect/intuit. The Rust primitive types (u32
, char
, u8
), and the &
and &mut
reference
types all implement Copy
and thus "opt-out" of the "move" semantic . The MyData
is like Vec<T>
*** and "everything else" per the default move semantic in Rust. Again though, whether or not we "opt-out" of the move semantic , we aren't changing what gets copied when .
*** `Copy` and `Drop` are mutually exclusive
Vec<T>
and the like do not implement Copy
, nor can it be made to do so because it hosts an internal pointer to memory located on the heap; creating a bit-wise copy would effectively create an alias, defeating the purpose of the accounting. More generally, the copies would point to the same, now shared, resource. This applies equally to any resource that needs to be formally de-allocated or closed. This latter spec is specified with the Drop
trait. And thus, the mutual exclusivity between Drop
and Copy
implementations - it's one or the other. Finally, Vec<T>
does implement Clone
. The implementation provides a new heap allocation and internal pointers with different values than the original... point to the new memory. The new clone will generate its own call to drop
to its own separate resources.