Borrowing instead of copying

Just trying to understand this better in chapter 5.2

in this example:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(rectangle: Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

// I got rid of the references '&' as I wanted to ask something

Inside the area function, why by default does the rectangle variable take ownership of the rect1's values? Why doesn't it just copy its values by default?

Copy is not the default because it would be a breaking change to later make a type not Copy. If you want to make a type Copy, you can easily implement it with #[derive(Clone, Copy)] right before your type definition.

#[derive(Clone, Copy)]
struct Rectangle {
    width: u32,
    height: u32,
} 
1 Like

I understand that.

But I want to know the reasons why doesn't it copy by default.

What do you mean by this exactly?

Simply stating, let's check this code (your own example, slightly modified):

struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
    println!(
        "Repeating: the area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(rectangle: Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Imagine that 1) Copy is enabled by default, so this compiles (now it won't compile obviously), and 2) the struct definition and the main function are in fact in different crates (so that the user of the struct doesn't control its definition).

Then, perhaps you need to add some kind of label to the rectangle. You simply change the struct definition to the following:

struct Rectangle {
    width: u32,
    height: u32,
    label: String,
}

...and the downstream code instantly breaks, since String is not Copy, and so the new Rectangle can not be Copy, too. But this is an implicit breakage: if you don't use yourself the fact that Rectangle was Copy, you probably wouldn't even notice it before the library user raises an issue. Implicit breakages are one of the things that Rust is trying to make impossible, and that seems to be the main reason why Copy is not an auto-trait and must be explicitly implemented (or derived).

4 Likes

I see now, thanks for explaining this.

Just out of curiosity, how come if I do this with Strings:

let x = String::from("Hello");
let y = x; // This moves the data from x, to y

Why doesn't x just copies the data stored on the stack (besides the pointer) and the heap and move it into y and if the user wants to actually move it then we can call a function to do that instead?

that's exactly what happens. A String is just length, capacity, and pointer to the heap data.

when doing y = x, you are moving those two integers and a pointer. It can't be the case that both x and y have ownership of the same pointer or else we would get a double free at the end of the scope.

1 Like

Because copying data in heap isn't cheap (think if this String contains the whole text for "The Lord of the Rings" or something of a similar size), and Copy trait is about copying that is cheap (and can be done implicitly).

1 Like

Another angle to consider - passing a reference is an act of copying as well (of the reference itself, not the data it points to).

So things that are around the size of a pointer or two are often Copy since there's no real benefit to passing a reference (i.e. it'd be the same amount of churn, with the added constraint of the borrow checker kicking in), whereas things that are larger than that are usually not Copy since it's cheaper to pass a reference.

Hope I got that right...

2 Likes

So is it cheaper to do this:

fn main()
{
    let (x, y) = (10, 20);
    area(&x, &y);
}

fn area(x:&u32, y:&u32)
{
    // Does something
}

Than this:

fn main()
{
    let (x, y) = (10, 20);
    area(x, y);
}

fn area(x:u32, y:u32)
{
    // Does something
}

??

In principle the second would probably be cheaper, but it's likely that the compiler optimizes the first to the second.

WHy would the 2nd be cheaper?

Well when area needs to read the value of x, it would need to do a memory lookup through a pointer, that the second doesn't need to do.

1 Like

u32 does not qualify as "large", by a long shot. The reference, represented as a pointer, is twice as large as the u32 on 64-bit architectures.

1 Like

Clippy defines a threshold of 8 bytes as cheap, e.g. everything below 8 bytes should be copy, everything above should be clone.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.