Beginner questions about references

I'm starting to learn rust following "The Book" and I have a few questions about how referencing variables work. The examples of passing a variable to a function are more or less clear to me, if I pass the variable, the owner of the data is now inside the function scope and it will go away when the function ends, if I use a reference, the ownership of the data only changes during the execution of the function and the it goes back, got it.

My questions are regarding other cases. I read that when using the dot syntax with methods and attributes, rust will take care of referencing and dereferencing variables appropriately, but I have the following questions:

  • What happens with operators like < for example, when I use them on a variable, does the ownership of the data goes inside the definition of the operator as it happens with functions or does rust also automatically creates a reference so I can keep the ownership of the data in the scope outside the operator?

  • How does ownership work with for loops? What is happening when I do

    let my_array = [1, 2, 3];
    
    for i in my_array {
        println!("{}", i);
    }
    

    Is the for automatically creating a reference to the array?

  • Similarly, how do vectors from the collection library work? In the book to retrieve the element of a vector the following syntax is used:

    let my_vec = vec![1, 2, 3];
    println!("{}", &my_vec[0]);
    

    but this also work:

    println!("{}", my_vec[0]);
    

    so, again, is rust creating the reference for me?

1 Like

Rust will automatically create a reference; something like a < b or a == b never takes ownership. Note that this is not the case for other operators like +; a + b and a * b does take ownership.

Rust will take ownership of the value after the in. In your case, the array is Copy so you don't notice it, but when the array isn't it will be consumed.

Yes, in that example it is println! that automatically creates the reference. If you tried std::convert::identity(my_vec[0]) it would fail if the vector did not contain Copy items, because you can never move out of a container with indexing syntax.

3 Likes

Just to supplement this: if you're uncertain about a particular operator, you can look up its corresponding trait in std::ops. If the method takes self it is by value, but if it takes &self the compiler will automatically borrow it for you.

for loops use the IntoIterator trait to turn my_array into an iterator. into_iter takes self, so this is again a by-value conversion. But it is common for references themselves to implement IntoIterator, so for i in &my_array would also work (but means something slightly different, since the iterable is a different type).

4 Likes

Just adding on to the above replies, the trait for < and > is PartialOrd and for == is PartialEq; as you can see, their methods always take references.

For the operators corresponding to methods that take by value such as Add, it's still possible to have implementations that work with references, so they may or may not consume their values. (And of course, consuming a Copy type will just consume a copy if you still need the value.)

The trait for indexing is Index (or IndexMut). Be careful to read the documentation there -- even though the method is returning a reference, container[i] is short for *container.index(i). So if you want to save a reference, you need something like &container[i]. (This is similar to C, where ptr[i] is *(ptr+i).)

Macros may act differently from one another, so I recommend not inferring how the language works in general from how a macro like println! behaves. For example, this doesn't compile:

#[derive(Debug)]
struct Foo(i32);

fn main() {
    let my_vec = vec![Foo(0)];
    dbg!(my_vec[0]);
}

Because dbg! doesn't create a reference.

3 Likes

Thank you for your complete answers, I now understand that the behaviour of operators depends on their trait, which is not always defined in the same way.

if I understand it right, operators like PartialEq are implemented as methods of the first arguments (&self) that take another argument, other. Because other is of type &Rhs both arguments are references and thus the methods don't take ownership of any of the arguments.

On the other hand, Add uses self and Rhs (no references) and thus it takes ownership.

This design choice seems a bit odd to me, what is the reasoning behind choosing when an operator takes ownership and when it doesn't? Is it because when comparison operators are used one can expect that the variables will be used later on but when combination operators are used (like +), the result of the combination usually take the place of the original variables?

I have also tested the behaviour of the for loops and accessing the elements of a vector when the elements are not Copy and without using macros and, as you said, this for loop:

let my_array = [String::from("one"), String::from("two"), String::from("three")];

for i in my_array {

}

takes ownership of the array and I can't use later on and this code:

let my_vec = vec![String::from("one"), String::from("two"), String::from("three")];

let my_var = my_vec[0];

doesn't compile. My question here is, does that mean that a collection of elements of a type that is Copy is itself Copy?

I think it's more that some operations are more efficient if you consume one or both of the arguments, modify them, and then return one of them. Consider:

let f_ = "foo".to_string();
let fb = f_ + "bar";

Whereas the output type of == is always bool.

No, not generally; you can change your first example to use integers and it still won't compile. Vec is never copy for example, because it consists of two usizes (length and capacity) plus a pointer into the heap. If you could copy it, you would have multiple pointers to the same memory, which would cause problems (multiple mutable aliases, double-free on drop, etc.) Instead you have to clone it.

Depending on how you define "container", there may be some which are Copy if their contents are Copy. For example, an Option or a tuple.

Your second example will compile if you use integers because you will copy out the integer. You're not copying the Vec as a whole in that case.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.