prior
prior

Reputation: 518

How to decide when function input params should be references or not?

When writing a function how does one decide whether to make input parameters referenced or consumed?

For example, should I do this?

fn foo(val: Bar) -> bool { check(val) } // version 1

Or use referenced param instead?

fn foo(val: &Bar) -> bool { check(*val) } // version 2

On the client side, if I only had the second version but wanted to have my value consumed, I'd have to do something like this:

// given in: Bar
let out = foo(&in); // using version 2 but wanting to consume ownership
drop(in);

On the other hand, if I only had the first version but wanted to keep my reference, I'd have to do something like this:

// given in: &Bar
let out = foo(in.clone()); // using version 1 but wanting to keep reference alive

So which is preferred, and why?

Are there any performance considerations in making this choice? Or does the compiler make them equivalent in terms of performance, and how?

And when would you want to offer both versions (via traits)? And for those times how do you write the underlying implementations for both functions -- do you duplicate the logic in each method signature or do you have one proxy to the other? Which to which, and why?

Upvotes: 29

Views: 8069

Answers (2)

Schwern
Schwern

Reputation: 164829

Rust's goal is to have performance and syntax similar to C/C++ without the memory problems. To do this it avoids things like garbage collection and instead enforces a particular strict memory model of "ownership" and "borrowing". These are critical concepts in Rust. I would suggest reading Understanding Ownership in The Rust Book.

The rules of memory ownership are...

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Enforcing a single owner avoids a great many bugs and complications typical of C and C++ programs while avoiding complex and slow memory management at runtime.

You can't get very far with only that, so Rust provides references. A reference lets functions safely "borrow" data without taking ownership. You can have either as many immutable references as you like, or only one mutable reference.


When applied to function calls, passing a value passes ownership to the function. Passing a reference is "borrowing", ownership is retained.

It's really, really important to understand ownership, borrowing, and later on, lifetimes. But here's some rules of thumb.

  • If your function needs to take ownership of the data, pass by value.
  • If your function only needs to read the data, pass a reference.
  • If your function needs to change the data, pass a mutable reference.

Note what's not in there: performance. Let the compiler take care of that.

Assuming check only reads data and checks that it's ok, it should take a reference. So your example would be...

fn foo(val: &Bar) -> bool { check(val) }

On the client side, if I only had the second version but wanted to have my value consumed...

There's no reason to want a function which takes a reference to do that. If it's the function's job to manage the memory, you pass it ownership. If it isn't, it's not its job to manage your memory.

There's also no need to manually call drop. You'd simply let the variable fall out of scope and it will be automatically dropped.

And when would you want to offer both versions (via traits)?

You wouldn't. If a function can take a reference there's no reason for it to take ownership.

Upvotes: 27

kmdreko
kmdreko

Reputation: 60072

If the function needs ownership, you should pass by value. If the function only needs a reference, you should pass by reference.

Passing by value fn foo(val: Bar) when it isn't necessary for the function to work could require the user to clone the value. Passing by reference is preferred in this case since a clone can be avoided.

Passing by reference fn foo(val: &Bar) when the function needs ownership would require it to either copy or clone the value. Pass by value is preferred in this case because it gives the user control whether an existing value's ownership is transferred or is cloned. The function doesn't have to make that decision and a clone can be avoided.

There are some exceptions, simple primitives like i32 can be passed-by-value without any performance penalty and may be more convenient.


And when would you want to offer both versions (via traits)?

You could use the Borrow trait:

fn foo<B: Borrow<Bar>>(val: B) -> bool {
    check(val.borrow())
}

let b: Bar = ...;
foo(&b); // both of
foo(b);  // these work

Upvotes: 12

Related Questions