Operators and Expressions

Function Call Expressions

A function call expression invokes a function, method, closure, or tuple-struct/enum-variant constructor with a comma-separated list of argument expressions enclosed in parentheses (...) and evaluates to the function's return value.

Function Call Syntax Forms

Rust supports four forms of call expressions:

Call Form Syntax Example
Free function call path::to::func(arg1, arg2) usize::from_str_radix("FF", 16)
Method call (dot syntax) receiver.method(arg1, arg2) " hello ".trim()
Universal Function Call Syntax (UFCS) Type::method(&receiver, arg1) str::trim(" hello ")
Closure / Function-pointer call callable_expr(arg1, arg2) (ops[0])(10, 20)
fn hypot_squared(dx: i32, dy: i32) -> i32 {
    dx * dx + dy * dy
}

fn main() {
    // 1. Free function call
    let dist_sq = hypot_squared(3, 4);

    // 2. Method call vs equivalent Associated Function (UFCS) call
    let text = "  rust  ";
    let via_method = text.trim();
    let via_ufcs = str::trim(text);

    assert_eq!(via_method, via_ufcs);
    println!("dist_sq = {dist_sq}, trimmed = '{via_method}'");
}
dist_sq = 25, trimmed = 'rust'

Left-to-Right Evaluation Order of Arguments

Unlike C and C++—where the order in which function arguments f(a(), b(), c()) are evaluated is unspecified—Rust strictly guarantees that both the callable expression and all argument expressions are evaluated from left to right before the function body begins executing:

Call Expression:   compute( step_1(),  step_2(),  step_3() )
Evaluation Order:  ───────  ────────   ────────   ────────
                     (1)      (2)        (3)        (4)     ---> Then enters `compute` body!
fn arg(name: &str, val: i32) -> i32 {
    println!("  evaluating argument {name} -> {val}");
    val
}

fn combine(a: i32, b: i32, c: i32) -> i32 {
    println!("  inside combine({a}, {b}, {c})");
    a + b + c
}

fn main() {
    let total = combine(arg("first", 10), arg("second", 20), arg("third", 30));
    println!("Total: {total}");
}
  evaluating argument first -> 10
  evaluating argument second -> 20
  evaluating argument third -> 30
  inside combine(10, 20, 30)
Total: 60

Value Passing Semantics: Copy vs Move vs Borrow

When an argument expression is passed to a function parameter, Rust applies the exact same ownership rules as a let assignment (let param = arg;):

Parameter Type in fn Caller Argument What Happens at Call Site Caller Variable Usable Afterward?
T (where T: Copy, e.g., i32) x Bitwise copy of x is passed into parameter Yes
T (where T is not Copy, e.g., String) s Ownership of s moves into parameter No (E0382 if used again)
&T (shared borrow) &s 8-byte (or 16-byte slice) shared reference passed Yes (read-only during call)
&mut T (mutable borrow) &mut s Exclusive mutable reference passed Yes ( holds modified value after call!)
fn consume_string(owned: String) {
    println!("Consumed: {owned}");
} // `owned` is dropped here!

fn borrow_string(shared: &str) {
    println!("Borrowed: {shared}");
}

fn main() {
    let message = String::from("persistent data");

    // 1. Pass by shared reference (`&message` coerces to `&str`): `message` stays valid
    borrow_string(&message);

    // 2. Pass by value: ownership of `message` moves into `consume_string`
    consume_string(message);

    // `message` can no longer be used after `consume_string(message)`!
}
Borrowed: persistent data
Consumed: persistent data

Automatic Deref Coercions at Call Sites

When you pass a reference (&T or &mut T) as a function argument, the compiler automatically applies deref coercion if the parameter expects a target type reachable via the Deref trait:

  • &String automatically coerces to &str.
  • &Vec<T> and &[T; N] automatically coerce to &[T].
  • &Box<T>, &Rc<T>, and &Arc<T> automatically coerce to &T.
  • &mut T automatically coerces to an immutable &T when the function only needs read access.
fn print_items(slice: &[i32], label: &str) {
    println!("{label}: {slice:?}");
}

fn main() {
    let owned_vec: Vec<i32> = vec![1, 2, 3];
    let owned_label: String = String::from("Vector as slice");

    // `&owned_vec` (`&Vec<i32>`) coerces to `&[i32]`, and `&owned_label` (`&String`) coerces to `&str`:
    print_items(&owned_vec, &owned_label);
}
Vector as slice: [1, 2, 3]

No Named, Default, or Variadic Arguments in Rust Functions

Unlike Python or C++, regular Rust functions (fn) have fixed arity:

  • No default parameter values: Every declared parameter must be supplied at every call site.
  • No variadic functions: Functions cannot accept a variable number of arguments (macros like println! and vec! handle variadic syntax at compile time, or functions accept a slice &[T]).
  • Trailing comma allowed: A trailing comma after the last argument (compute(10, 20,)) is always valid and standard in multi-line calls.

Passing the wrong number of arguments fails at compile time with E0061:

// ❌ does not compile
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(10));
}
error[E0061]: this function takes 2 arguments but 1 argument was supplied
 --> src/main.rs:6:20
  |
6 |     println!("{}", add(10));
  |                    ^^^---- argument #2 of type `i32` is missing
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |    ^^^         ------
help: provide the argument
  |
6 |     println!("{}", add(10, /* i32 */));
  |                       ~~~~~~~~~~~~~~~

error: aborting due to 1 previous error