Why does `ref` in a function parameter move out of the ownership of the argument?

Consider this example:

fn show(ref v:String){}
fn main(){
  let s = "abc".to_string();
  show(s);
  println!("{s}"); // #1 borrow of moved value: `s`
}

In this example, #1 have an error. Then, as a contrast example:

fn main(){
  let s = "abc".to_string();
  let ref v:String = s;
  println!("{s}"); // #2 Ok
}

#2 is ok. It appears to me that such two examples are identical except that, in the first example, we passed s to the parameter ref v:String of the function show, and in the second example, we passed s to the let binding ref v:String. Anyway, such two bindings both with the same form ref v:String, however, one takes ownership of s and the other does not. What causes the difference?

1 Like

In brief as I'm about to step away:

In the second example, the expression to the right of = is a place expression and s doesn't get moved as ref v does not bind by value.

In the first example, the function still takes a String by value and it acts notionally something like

fn show(__v: String) {
    let ref v: String = __v;
}
3 Likes

In the first example, ref v:String was also declared as taking a String by value if we admit ref v:String in the function's parameter takes a String by value, why should we treat them differently?

(On mobile)

The pattern (so binding modifiers) of a function parameter doesn't change the function API, and the API takes parameters by value. Personally I'd find it surprising and fragile if things worked otherwise.

Regardless, it'd be a breaking change now - the destructor needs to run by the time the function returns. And also, the function can remove ref without breaking any caller; that ability would be taken away.

3 Likes

You say that the argument to show is a String. The ref v part is irrelevant as far as the calling code is concerned. So, show(s) passes s by value into show.

The body of show then does a by-ref binding to this argument, storing this &String in v. Again, that this happens does not concern the code in main at all. Also note that v is not of type String, it's of type &String because of that ref. The type specifier is essentially declaring what the type of the entire initialising expression on the right of the = is.

You can see this in a playpen that prints out the type name of the v binding.

If that seems weird, consider the following:

let (x, ref y): (i32, i32) = (1, 2);

This results in two bindings: x: i32 and y: &i32.

The second example works because the compiler doesn't need to move s to fulfill the binding, it just needs to borrow it.

3 Likes

So, why does ref v:String in let binding make v be of type &String but the ref v:String in the function's parameter does not?

It does. But it doesn't make the function not take the parameter by value.

3 Likes

Consider the following:

fn hypot_1(xy: (f64, f64)) -> f64 {
    (xy.0*xy.0 + xy.1*xy.1).sqrt()
}

fn hypot_2((x, y): (f64, f64)) -> f64 {
    (x*x + y*y).sqrt()
}

fn hypot_3(ref xy: (f64, f64)) -> f64 {
    (xy.0*xy.0 + xy.1*xy.1).sqrt()
}

fn hypot_4((ref x, ref y): (f64, f64)) -> f64 {
    (x*x + y*y).sqrt()
}

fn main() {
    println!("{}", hypot_1((2.0, 3.0)));
    println!("{}", hypot_2((2.0, 3.0)));
    println!("{}", hypot_3((2.0, 3.0)));
    println!("{}", hypot_4((2.0, 3.0)));
}

hypot_1 through hypot_4 have exactly the same signature. They all take exactly one argument of type (f64, f64). The use of a destructuring pattern in hypot_2, _3, and _4 is completely irrelevant to the calling code. That pattern matching happens only in the body of the function; the calling code neither sees it, nor cares about it.

The type of the binding and the type of the value that binding is derived from are not necessarily the same.

4 Likes

So, why does the type of let ref V:String = xxx; has the type &String?

Because of the ref. That says "the value that is in this position? Take a reference to it and call it v".

That's why I've been using tuples to try and show that let doesn't create a variable binding, it lets you pattern match against a value which can include creating variable bindings to parts of that value.

As far as I know, ref can be understood as a syntax sugar (though there may be some edge cases I have missed).
To aid you in understanding, this:

let ref foo = Foo;
takes_a_ref(foo);

is equivalent to:

let foo = Foo;
takes_a_ref(&foo);

This is why I'm keeping asking why there is a different treatment between let-bindings and parameters.

You say ref in let-binding makes v has a reference type, but the ref in the parameter does not. This is what I don't understand.

There isn't. They do the same thing.

#![feature(type_name_of_val)]
use std::any::type_name_of_val;

fn show_1(v: String) {
    let ty_name = type_name_of_val(&v);
    println!("{v}: {}", ty_name);
}

fn show_2(ref v: String) {
    let ty_name = type_name_of_val(&v);
    println!("{v}: {}", ty_name);
}

fn main() {
    let s = String::from("1"); show_1(s);
    let s = String::from("2"); show_2(s);

    {
        let v = String::from("3");
        let ty_name = type_name_of_val(&v);
        println!("{v}: {}", ty_name);
    }

    {
        let ref v = String::from("4");
        let ty_name = type_name_of_val(&v);
        println!("{v}: {}", ty_name);
    }
}

When run with a nightly compiler outputs:

1: alloc::string::String
2: &alloc::string::String
3: alloc::string::String
4: &alloc::string::String
1 Like

There is not.

let ref foo: String = String::new();
// foo is & String
// type specified after colon is the type of the value on the right side of =, 
// NOT of the created binding. 

the part before the semicolon is a PATTERN, and the part after is the TYPE of the pattern. it's just the PATTERN part is usually a simple identifier so it is confusing, it would be easier to understand if you use a more complex pattern. take this example, can you see the difference?

#[derive(Debug)]
struct MyString {
    inner: String
}
// there's short hand sugar syntax, but here I use the full syntax
fn show1(&MyString { inner: ref inner }: &MyString){let _ = inner;}
fn show2(MyString { inner: ref inner }: MyString){let _ = inner;}
fn main(){
  let s = "abc".to_string();
  let s = MyString {
      inner: s
  };

  show1(&s);
  println!("{s:?}"); // totally ok

  show2(s);
  //println!("{s:?}"); // error: borrow of moved value: `s`
}

in your original example, note the difference:

fn show1(ref v:String){}
fn show2(ref v: &String){}
1 Like

From this perspective, that means ref v:String in let-binding does not act the same as ref v:String that appears in parameter?

I just want to add, there's also the converse of the ref pattern: the & pattern, but that only works on Copy type, since you cannot move a non-Copy value out of a reference:

fn foo1(&x: &isize) {} // fine
fn foo2(&x: &String) {} // error
fn foo3(&_: &String) {} // actually fine, because `_` pattern is special

fn main() {
    foo1(&42); // correct
    foo1(42); // error
}

The right side of an assignment doesn't act the same as the explicitly annotated parameter of a function.

1 Like

So, you meant, if ref v:String appears on the left side of an assignment, then it means v binding to value expression without taking its ownership, rather, if it appears in the parameter, it means the function takes the ownership of the corresponding argument?

After reading the OP again, I found that your question was actually more subtle than it appeared at first. Sorry for missing the point.

Rust's documentation is quite incomplete, and the Reference doesn't make this explicit, but indeed, this has to do with place expressions.

I don't see any inherent reason that require function parameters to be in value expression context. In theory, the language could add the ability for functions to receive places without it decaying into values by using a different syntax. But I think it would be quite a hard feature to implement, as it's uncomfortably close to being full-fledged pattern types.

I would consider the compile error on #1 to be a limitation of rustc and nothing more, while #2 is expected.

2 Likes